├── src └── mutt_language_server │ ├── py.typed │ ├── assets │ └── queries │ │ └── import.scm │ ├── _shtab.py │ ├── __init__.py │ ├── finders.py │ ├── utils.py │ ├── __main__.py │ ├── schema.py │ ├── server.py │ └── misc │ └── __init__.py ├── templates ├── noarg.txt ├── class.txt ├── def.txt └── metainfo.py.j2 ├── requirements ├── misc.txt ├── colorize.txt └── dev.txt ├── .gitlint ├── docs ├── api │ └── mutt-language-server.md ├── requirements.txt ├── resources │ ├── requirements.md │ ├── install.md │ └── configure.md ├── index.md └── conf.py ├── requirements.txt ├── .yamllint.yaml ├── tests └── test_utils.py ├── .readthedocs.yaml ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .pre-commit-config.yaml ├── .gitignore ├── pyproject.toml ├── README.md └── LICENSE /src/mutt_language_server/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mutt_language_server/assets/queries/import.scm: -------------------------------------------------------------------------------- 1 | (path) @path 2 | -------------------------------------------------------------------------------- /templates/noarg.txt: -------------------------------------------------------------------------------- 1 | r"""{{ name.replace("_", " ").strip().capitalize() }}.""" 2 | -------------------------------------------------------------------------------- /templates/class.txt: -------------------------------------------------------------------------------- 1 | r"""{{ name.replace("_", " ").strip().capitalize() }}.""" 2 | 3 | -------------------------------------------------------------------------------- /requirements/misc.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pip install -r 2 | 3 | lsp-tree-sitter[misc] 4 | -------------------------------------------------------------------------------- /requirements/colorize.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pip install -r 2 | 3 | lsp-tree-sitter[colorize] 4 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pip install -r 2 | # For unit test and code coverage rate test. 3 | 4 | pytest-cov 5 | -------------------------------------------------------------------------------- /.gitlint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S gitlint -C 2 | [ignore-by-title] 3 | regex=.* 4 | ignore=body-is-missing 5 | # ex: filetype=dosini 6 | -------------------------------------------------------------------------------- /docs/api/mutt-language-server.md: -------------------------------------------------------------------------------- 1 | # mutt-language-server 2 | 3 | ```{autofile} ../../src/*/*.py 4 | --- 5 | module: 6 | --- 7 | ``` 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pip install -r 2 | 3 | lsp-tree-sitter >= 0.1.0 4 | pygls >= 2.0.0 5 | tree-sitter-muttrc >= 0.0.4 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pip install -r 2 | 3 | -e . 4 | myst-parser 5 | sphinxcontrib-autofile 6 | sphinxcontrib-requirements-txt 7 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S yamllint -c 2 | --- 3 | extends: default 4 | 5 | rules: 6 | comments: 7 | # https://github.com/prettier/prettier/issues/6780 8 | min-spaces-from-content: 1 9 | -------------------------------------------------------------------------------- /docs/resources/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | ```{requirements} ../../requirements.txt 4 | --- 5 | title: Mandatory Requirements 6 | --- 7 | ``` 8 | 9 | ```{requirements} ../../requirements/*.txt 10 | --- 11 | --- 12 | ``` 13 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | r"""Test utils.""" 2 | 3 | from mutt_language_server.utils import get_schema 4 | 5 | 6 | class Test: 7 | r"""Test.""" 8 | 9 | @staticmethod 10 | def test_get_schema() -> None: 11 | assert get_schema()["properties"].get("set") 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 2 | --- 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3" 9 | 10 | formats: 11 | - htmlzip 12 | 13 | python: 14 | install: 15 | - requirements: docs/requirements.txt 16 | -------------------------------------------------------------------------------- /templates/def.txt: -------------------------------------------------------------------------------- 1 | r"""{{ name.replace("_", " ").strip().capitalize() }}. 2 | 3 | {% for p in params -%} 4 | :param {{ p.argument }}: 5 | {% if p.annotation -%} 6 | :type {{ p.argument }}: {{ p.annotation.strip('"') }} 7 | {% endif -%} 8 | {% endfor -%} 9 | {% if return_type -%} 10 | :rtype: {{ return_type }} 11 | {% endif -%} 12 | """ 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | custom: 3 | - "https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png" 4 | - "https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png" 5 | - "https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png" 6 | -------------------------------------------------------------------------------- /src/mutt_language_server/_shtab.py: -------------------------------------------------------------------------------- 1 | r"""Fake shtab 2 | ============== 3 | """ 4 | 5 | from argparse import ArgumentParser 6 | from typing import Any 7 | 8 | FILE = None 9 | DIRECTORY = DIR = None 10 | 11 | 12 | def add_argument_to(parser: ArgumentParser, *args: Any, **kwargs: Any): 13 | from argparse import Action 14 | 15 | Action.complete = None # type: ignore 16 | return parser 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```{toctree} 4 | --- 5 | hidden: 6 | glob: 7 | caption: resources 8 | --- 9 | resources/* 10 | ``` 11 | 12 | ```{toctree} 13 | --- 14 | hidden: 15 | glob: 16 | caption: API 17 | --- 18 | api/* 19 | ``` 20 | 21 | ```{toctree} 22 | --- 23 | hidden: 24 | caption: Index 25 | --- 26 | genindex 27 | modindex 28 | search 29 | ``` 30 | 31 | ```{include} ../README.md 32 | --- 33 | relative-docs: docs 34 | --- 35 | ``` 36 | -------------------------------------------------------------------------------- /src/mutt_language_server/__init__.py: -------------------------------------------------------------------------------- 1 | r"""Provide ``__version__`` for 2 | `importlib.metadata.version() `_. 3 | """ 4 | 5 | from typing import Literal 6 | 7 | try: 8 | from ._version import __version__, __version_tuple__ # type: ignore 9 | except ImportError: # for setuptools-generate 10 | __version__ = "rolling" 11 | __version_tuple__ = (0, 0, 0, __version__, "") 12 | 13 | __all__ = ["__version__", "__version_tuple__"] 14 | FILETYPE = Literal["neomuttrc"] 15 | -------------------------------------------------------------------------------- /templates/metainfo.py.j2: -------------------------------------------------------------------------------- 1 | """This file is generated by setuptools-generate. 2 | The information comes from pyproject.toml. 3 | It provide some metainfo for docs/conf.py to build documents and 4 | help2man to build man pages. 5 | """ 6 | 7 | # For docs/conf.py 8 | project = "{{ data['project']['name'] }}" 9 | author = """{% for author in data['project']['authors'] -%} 10 | {{ author['name'] }} <{{ author['email'] }}> {% endfor -%} 11 | """ 12 | copyright = "{{ year }}" 13 | 14 | # For help2man 15 | DESCRIPTION = "{{ data['project']['description'] }}" 16 | EPILOG = "Report bugs to {{ data['project']['urls']['Bug Report'] }}" 17 | # format __version__ by yourself 18 | VERSION = """{{ data['project']['name'] }} {__version__} 19 | Copyright (C) {{ year }} 20 | Written by {% for author in data['project']['authors'] -%} 21 | {{ author['name'] }} <{{ author['email'] }}> {% endfor %}""" 22 | SOURCE = "{{ data['project']['urls']['Source'] }}" 23 | -------------------------------------------------------------------------------- /docs/resources/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ## [AUR](https://aur.archlinux.org/packages/mutt-language-server) 4 | 5 | ```sh 6 | yay -S mutt-language-server 7 | ``` 8 | 9 | ## [NUR](https://nur.nix-community.org/repos/Freed-Wu) 10 | 11 | ```nix 12 | { config, pkgs, ... }: 13 | { 14 | nixpkgs.config.packageOverrides = pkgs: { 15 | nur = import 16 | ( 17 | builtins.fetchTarball 18 | "https://github.com/nix-community/NUR/archive/master.tar.gz" 19 | ) 20 | { 21 | inherit pkgs; 22 | }; 23 | }; 24 | environment.systemPackages = with pkgs; 25 | ( 26 | python3.withPackages ( 27 | p: with p; [ 28 | nur.repos.Freed-Wu.mutt-language-server 29 | ] 30 | ) 31 | ) 32 | } 33 | ``` 34 | 35 | ## [Nix](https://nixos.org) 36 | 37 | ```sh 38 | nix shell github:neomutt/mutt-language-server 39 | ``` 40 | 41 | Run without installation: 42 | 43 | ```sh 44 | nix run github:neomutt/mutt-language-server -- --help 45 | ``` 46 | 47 | ## [PYPI](https://pypi.org/project/mutt-language-server) 48 | 49 | ```sh 50 | pip install mutt-language-server 51 | ``` 52 | 53 | See [requirements](requirements) to know `extra_requires`. 54 | -------------------------------------------------------------------------------- /src/mutt_language_server/finders.py: -------------------------------------------------------------------------------- 1 | r"""Finders 2 | =========== 3 | """ 4 | 5 | from dataclasses import dataclass 6 | 7 | from lsp_tree_sitter.finders import ErrorFinder, QueryFinder, SchemaFinder 8 | from lsprotocol.types import DiagnosticSeverity 9 | 10 | from .schema import MuttTrie 11 | from .utils import get_query, get_schema 12 | 13 | 14 | @dataclass(init=False) 15 | class ImportMuttFinder(QueryFinder): 16 | r"""Import mutt finder.""" 17 | 18 | def __init__( 19 | self, 20 | message: str = "{{uni.text}}: found", 21 | severity: DiagnosticSeverity = DiagnosticSeverity.Information, 22 | ): 23 | r"""Init. 24 | 25 | :param message: 26 | :type message: str 27 | :param severity: 28 | :type severity: DiagnosticSeverity 29 | """ 30 | query = get_query("import") 31 | super().__init__(query, message, severity) 32 | 33 | 34 | @dataclass(init=False) 35 | class MuttFinder(SchemaFinder): 36 | r"""Muttfinder.""" 37 | 38 | def __init__(self) -> None: 39 | r"""Init. 40 | 41 | :rtype: None 42 | """ 43 | self.validator = self.schema2validator(get_schema()) 44 | self.cls = MuttTrie 45 | 46 | 47 | DIAGNOSTICS_FINDER_CLASSES = [ 48 | ErrorFinder, 49 | MuttFinder, 50 | ] 51 | -------------------------------------------------------------------------------- /src/mutt_language_server/utils.py: -------------------------------------------------------------------------------- 1 | r"""Utils 2 | ========= 3 | """ 4 | 5 | import json 6 | import os 7 | from typing import Any 8 | 9 | from tree_sitter import Language, Parser, Query 10 | from tree_sitter_muttrc import language as get_language_ptr 11 | 12 | from . import FILETYPE 13 | 14 | SCHEMAS = {} 15 | QUERIES = {} 16 | parser = Parser() 17 | parser.language = Language(get_language_ptr()) 18 | 19 | 20 | def get_query(name: str, filetype: FILETYPE = "neomuttrc") -> Query: 21 | r"""Get query. 22 | 23 | :param name: 24 | :type name: str 25 | :param filetype: 26 | :type filetype: FILETYPE 27 | :rtype: Query 28 | """ 29 | if name not in QUERIES: 30 | with open( 31 | os.path.join( 32 | os.path.dirname(__file__), 33 | "assets", 34 | "queries", 35 | f"{name}{os.path.extsep}scm", 36 | ) 37 | ) as f: 38 | text = f.read() 39 | if parser.language: 40 | QUERIES[name] = Query(parser.language, text) 41 | else: 42 | raise NotImplementedError 43 | return QUERIES[name] 44 | 45 | 46 | def get_schema(filetype: FILETYPE = "neomuttrc") -> dict[str, Any]: 47 | r"""Get schema. 48 | 49 | :param filetype: 50 | :type filetype: FILETYPE 51 | :rtype: dict[str, Any] 52 | """ 53 | if filetype not in SCHEMAS: 54 | file = os.path.join( 55 | os.path.dirname(__file__), 56 | "assets", 57 | "json", 58 | f"{filetype}.json", 59 | ) 60 | with open(file) as f: 61 | SCHEMAS[filetype] = json.load(f) 62 | return SCHEMAS[filetype] 63 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | r"""Configure the Sphinx documentation builder. 2 | 3 | https://www.sphinx-doc.org/en/master/usage/configuration.html 4 | """ 5 | 6 | from mutt_language_server._metainfo import ( # type: ignore 7 | author, 8 | copyright, 9 | project, 10 | ) 11 | 12 | from mutt_language_server import __version__ as version # type: ignore 13 | 14 | __all__ = ["version", "author", "copyright", "project"] 15 | 16 | # -- Path setup -------------------------------------------------------------- 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | 22 | # -- Project information ----------------------------------------------------- 23 | language = "en" 24 | locale_dirs = ["locale"] 25 | gettext_compact = False 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | "sphinx.ext.autodoc", 34 | "sphinx.ext.githubpages", 35 | "sphinx.ext.napoleon", 36 | "sphinx.ext.todo", 37 | "sphinx.ext.viewcode", 38 | "myst_parser", 39 | "sphinxcontrib.autofile", 40 | "sphinxcontrib.requirements_txt", 41 | ] 42 | 43 | myst_heading_anchors = 3 44 | myst_title_to_header = True 45 | todo_include_todos = True 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ["_templates"] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 54 | 55 | 56 | # -- Options for HTML output ------------------------------------------------- 57 | 58 | # The theme to use for HTML and HTML Help pages. See the documentation for 59 | # a list of builtin themes. 60 | # 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | # html_static_path = ["_static"] 66 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude: ^templates/|\.json$ 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: fix-byte-order-marker 10 | - id: check-case-conflict 11 | - id: check-shebang-scripts-are-executable 12 | - id: check-merge-conflict 13 | - id: trailing-whitespace 14 | - id: mixed-line-ending 15 | - id: end-of-file-fixer 16 | - id: detect-private-key 17 | - id: check-symlinks 18 | - id: check-ast 19 | - id: debug-statements 20 | - id: requirements-txt-fixer 21 | - id: check-xml 22 | - id: check-yaml 23 | - id: check-toml 24 | - id: check-json 25 | - repo: https://github.com/Lucas-C/pre-commit-hooks 26 | rev: v1.5.5 27 | hooks: 28 | - id: remove-crlf 29 | - repo: https://github.com/codespell-project/codespell 30 | rev: v2.4.1 31 | hooks: 32 | - id: codespell 33 | additional_dependencies: 34 | - tomli 35 | - repo: https://github.com/jorisroovers/gitlint 36 | rev: v0.19.1 37 | hooks: 38 | - id: gitlint 39 | args: 40 | - --msg-filename 41 | - repo: https://github.com/editorconfig-checker/editorconfig-checker.python 42 | rev: 3.4.1 43 | hooks: 44 | - id: editorconfig-checker 45 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 46 | rev: 3.0.0 47 | hooks: 48 | - id: check-mailmap 49 | - repo: https://github.com/rhysd/actionlint 50 | rev: v1.7.8 51 | hooks: 52 | - id: actionlint 53 | - repo: https://github.com/adrienverge/yamllint 54 | rev: v1.37.1 55 | hooks: 56 | - id: yamllint 57 | - repo: https://github.com/executablebooks/mdformat 58 | rev: 1.0.0 59 | hooks: 60 | - id: mdformat 61 | additional_dependencies: 62 | - mdformat-pyproject 63 | - mdformat-gfm 64 | # - mdformat-myst 65 | - mdformat-toc 66 | - mdformat-deflist 67 | - mdformat-beautysh 68 | - mdformat-ruff 69 | - mdformat-config 70 | - mdformat-web 71 | - repo: https://github.com/DavidAnson/markdownlint-cli2 72 | rev: v0.18.1 73 | hooks: 74 | - id: markdownlint-cli2 75 | additional_dependencies: 76 | - markdown-it-texmath 77 | - repo: https://github.com/astral-sh/ruff-pre-commit 78 | rev: v0.14.3 79 | hooks: 80 | - id: ruff-check 81 | - id: ruff-format 82 | - repo: https://github.com/kumaraditya303/mirrors-pyright 83 | rev: v1.1.407 84 | hooks: 85 | - id: pyright 86 | 87 | ci: 88 | skip: 89 | - pyright 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _version.py 2 | _metainfo.py 3 | 4 | # create by https://github.com/iamcco/coc-gitignore (Sat Jun 17 2023 19:59:16 GMT+0800 (China Standard Time)) 5 | # pydev.gitignore: 6 | .pydevproject 7 | 8 | # create by https://github.com/iamcco/coc-gitignore (Sat Jun 17 2023 19:59:20 GMT+0800 (China Standard Time)) 9 | # Python.gitignore: 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # celery beat schedule file 103 | celerybeat-schedule 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | -------------------------------------------------------------------------------- /docs/resources/configure.md: -------------------------------------------------------------------------------- 1 | # Configure 2 | 3 | - For windows, change `~/.config` to `~/AppData/Local` 4 | - For macOS, change `~/.config` to `~/Library` 5 | 6 | ## (Neo)[Vim](https://www.vim.org) 7 | 8 | For vim: 9 | 10 | - Change `~/.config/nvim` to `~/.vim` 11 | - Change `init.vim` to `vimrc` 12 | 13 | ### [coc.nvim](https://github.com/neoclide/coc.nvim) 14 | 15 | `~/.config/nvim/coc-settings.json`: 16 | 17 | ```json 18 | { 19 | "languageserver": { 20 | "mutt": { 21 | "command": "mutt-language-server", 22 | "filetypes": [ 23 | "muttrc", 24 | "neomuttrc" 25 | ] 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ### [vim-lsp](https://github.com/prabirshrestha/vim-lsp) 32 | 33 | `~/.config/nvim/init.vim`: 34 | 35 | ```vim 36 | if executable('mutt-language-server') 37 | augroup lsp 38 | autocmd! 39 | autocmd User lsp_setup call lsp#register_server({ 40 | \ 'name': 'mutt', 41 | \ 'cmd': {server_info->['mutt-language-server']}, 42 | \ 'whitelist': ['muttrc', 'neomuttrc'], 43 | \ }) 44 | augroup END 45 | endif 46 | ``` 47 | 48 | ## [Neovim](https://neovim.io) 49 | 50 | `~/.config/nvim/init.lua`: 51 | 52 | ```lua 53 | vim.api.nvim_create_autocmd({ "BufEnter" }, { 54 | pattern = { "muttrc*", "neomuttrc" }, 55 | callback = function() 56 | vim.lsp.start({ 57 | name = "mutt", 58 | cmd = { "mutt-language-server" } 59 | }) 60 | end, 61 | }) 62 | ``` 63 | 64 | ## [Emacs](https://www.gnu.org/software/emacs) 65 | 66 | `~/.emacs.d/init.el`: 67 | 68 | ```lisp 69 | (make-lsp-client :new-connection 70 | (lsp-stdio-connection 71 | `(,(executable-find "mutt-language-server"))) 72 | :activation-fn (lsp-activate-on "muttrc" "neomuttrc") 73 | :server-id "mutt"))) 74 | ``` 75 | 76 | ## [Helix](https://helix-editor.com/) 77 | 78 | `~/.config/helix/languages.toml`: 79 | 80 | ```toml 81 | [[language]] 82 | name = "muttrc" 83 | language-servers = ["mutt-language-server"] 84 | 85 | [[language]] 86 | name = "neomuttrc" 87 | language-servers = ["mutt-language-server"] 88 | 89 | [language_server.mutt-language-server] 90 | command = "mutt-language-server" 91 | ``` 92 | 93 | ## [KaKoune](https://kakoune.org/) 94 | 95 | ### [kak-lsp](https://github.com/kak-lsp/kak-lsp) 96 | 97 | `~/.config/kak-lsp/kak-lsp.toml`: 98 | 99 | ```toml 100 | [language_server.mutt-language-server] 101 | filetypes = ["muttrc", "neomuttrc"] 102 | command = "mutt-language-server" 103 | ``` 104 | 105 | ## [Sublime](https://www.sublimetext.com) 106 | 107 | `~/.config/sublime-text-3/Packages/Preferences.sublime-settings`: 108 | 109 | ```json 110 | { 111 | "clients": { 112 | "mutt": { 113 | "command": [ 114 | "mutt-language-server" 115 | ], 116 | "enabled": true, 117 | "selector": "source.muttrc" 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | ## [Visual Studio Code](https://code.visualstudio.com/) 124 | 125 | [An official support of generic LSP client is pending](https://github.com/microsoft/vscode/issues/137885). 126 | 127 | ### [vscode-glspc](https://gitlab.com/ruilvo/vscode-glspc) 128 | 129 | `~/.config/Code/User/settings.json`: 130 | 131 | ```json 132 | { 133 | "glspc.serverPath": "mutt-language-server", 134 | "glspc.languageId": "muttrc" 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "on": 3 | push: 4 | paths-ignore: 5 | - "**.md" 6 | - docs/* 7 | pull_request: 8 | paths-ignore: 9 | - "**.md" 10 | - docs/* 11 | workflow_dispatch: 12 | 13 | # https://github.com/softprops/action-gh-release/issues/236 14 | permissions: 15 | contents: write 16 | 17 | env: 18 | PYTHONUTF8: "1" 19 | python-version: 3.x 20 | cache: pip 21 | 22 | jobs: 23 | test: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | runs-on: 28 | - ubuntu-latest 29 | - macos-latest 30 | - windows-latest 31 | runs-on: ${{matrix.runs-on}} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{env.python-version}} 37 | cache: ${{env.cache}} 38 | cache-dependency-path: |- 39 | requirements.txt 40 | requirements/dev.txt 41 | - name: Install dependencies 42 | run: | 43 | pip install -e '.[dev]' 44 | - name: Test 45 | run: | 46 | pytest --cov 47 | - uses: codecov/codecov-action@v4 48 | build: 49 | needs: test 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | runs-on: 54 | - ubuntu-latest 55 | - macos-latest 56 | - windows-latest 57 | runs-on: ${{matrix.runs-on}} 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: actions/setup-python@v5 61 | with: 62 | python-version: ${{env.python-version}} 63 | cache: ${{env.cache}} 64 | cache-dependency-path: |- 65 | requirements.txt 66 | requirements/dev.txt 67 | - name: Install dependencies 68 | run: | 69 | pip install build 70 | - name: Build 71 | run: | 72 | pyproject-build 73 | - uses: pypa/gh-action-pypi-publish@release/v1 74 | if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/') 75 | with: 76 | password: ${{secrets.PYPI_API_TOKEN}} 77 | - uses: actions/upload-artifact@v4 78 | if: runner.os == 'Linux' && ! startsWith(github.ref, 'refs/tags/') 79 | with: 80 | path: | 81 | dist/* 82 | - uses: softprops/action-gh-release@v2 83 | if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/') 84 | with: 85 | # body_path: build/CHANGELOG.md 86 | files: | 87 | dist/* 88 | 89 | deploy-aur: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | if: startsWith(github.ref, 'refs/tags/') 93 | steps: 94 | - uses: Freed-Wu/update-aur-package@v1.0.11 95 | with: 96 | package_name: mutt-language-server 97 | ssh_private_key: ${{secrets.AUR_SSH_PRIVATE_KEY}} 98 | 99 | deploy-nur: 100 | needs: build 101 | runs-on: ubuntu-latest 102 | if: startsWith(github.ref, 'refs/tags/') 103 | steps: 104 | - name: Trigger Workflow 105 | run: > 106 | curl -X POST -d '{"ref":"main"}' 107 | -H "Accept: application/vnd.github.v3+json" 108 | -H "Authorization: Bearer ${{ secrets.GH_TOKEN }}" 109 | https://api.github.com/repos/Freed-Wu/nur-packages/actions/workflows/version.yml/dispatches 110 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools_scm[toml] >= 6.2", "setuptools-generate >= 0.0.6"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html 6 | [project] 7 | name = "mutt-language-server" 8 | description = "mutt language server" 9 | readme = "README.md" 10 | # type_a | type_b 11 | requires-python = ">= 3.10" 12 | keywords = ["mutt", "language server"] 13 | classifiers = [ 14 | "Development Status :: 3 - Alpha", 15 | "Intended Audience :: Developers", 16 | "Topic :: Software Development :: Build Tools", 17 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 18 | "Operating System :: Microsoft :: Windows", 19 | "Operating System :: POSIX", 20 | "Operating System :: Unix", 21 | "Operating System :: MacOS", 22 | "Programming Language :: Python :: 3 :: Only", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: Implementation :: CPython", 28 | "Programming Language :: Python :: Implementation :: PyPy", 29 | ] 30 | dynamic = ["version", "dependencies", "optional-dependencies"] 31 | 32 | [[project.authors]] 33 | name = "Wu, Zhenyu" 34 | email = "wuzhenyu@ustc.edu" 35 | 36 | [project.license] 37 | text = "GPL v3" 38 | 39 | [project.urls] 40 | Homepage = "https://mutt-language-server.readthedocs.io" 41 | Download = "https://github.com/neomutt/mutt-language-server/releases" 42 | "Bug Report" = "https://github.com/neomutt/mutt-language-server/issues" 43 | Source = "https://github.com/neomutt/mutt-language-server" 44 | 45 | [project.scripts] 46 | mutt-language-server = "mutt_language_server.__main__:main" 47 | 48 | [tool.setuptools.package-data] 49 | mutt_language_server = ["py.typed", "assets/**"] 50 | 51 | [tool.setuptools.data-files] 52 | "share/man/man1" = ["sdist/mutt-language-server.1"] 53 | "share/bash-completion/completions" = ["sdist/mutt-language-server"] 54 | "share/zsh/site-functions" = ["sdist/_mutt-language-server"] 55 | 56 | [tool.setuptools.dynamic.dependencies] 57 | file = "requirements.txt" 58 | 59 | # begin: scripts/update-pyproject.toml.pl 60 | [tool.setuptools.dynamic.optional-dependencies.colorize] 61 | file = "requirements/colorize.txt" 62 | 63 | [tool.setuptools.dynamic.optional-dependencies.dev] 64 | file = "requirements/dev.txt" 65 | 66 | [tool.setuptools.dynamic.optional-dependencies.misc] 67 | file = "requirements/misc.txt" 68 | # end: scripts/update-pyproject.toml.pl 69 | 70 | [tool.setuptools_scm] 71 | write_to = "src/mutt_language_server/_version.py" 72 | 73 | [tool.setuptools-generate] 74 | write-to = "src/mutt_language_server/_metainfo.py" 75 | 76 | [tool.setuptools-generate.metainfo-template] 77 | file = "templates/metainfo.py.j2" 78 | 79 | [tool.mdformat] 80 | number = true 81 | 82 | [tool.doq] 83 | template_path = "templates" 84 | 85 | [tool.ruff] 86 | line-length = 79 87 | 88 | [tool.ruff.lint] 89 | select = [ 90 | # pycodestyle 91 | "E", 92 | # pyflakes 93 | "F", 94 | # pyupgrade 95 | "UP", 96 | # flake8-bugbear 97 | "B", 98 | # flake8-simplify 99 | "SIM", 100 | # isort 101 | "I", 102 | ] 103 | ignore = ["D205", "D400"] 104 | preview = true 105 | 106 | [tool.ruff.format] 107 | docstring-code-format = true 108 | preview = true 109 | 110 | [tool.coverage.report] 111 | exclude_lines = [ 112 | "if TYPE_CHECKING:", 113 | "if __name__ == .__main__.:", 114 | "\\s*import tomli as tomllib", 115 | ] 116 | -------------------------------------------------------------------------------- /src/mutt_language_server/__main__.py: -------------------------------------------------------------------------------- 1 | r"""This module can be called by 2 | `python -m `_. 3 | """ 4 | 5 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 6 | from datetime import datetime 7 | 8 | from . import FILETYPE, __version__ 9 | from . import __name__ as NAME 10 | 11 | try: 12 | import shtab 13 | except ImportError: 14 | from . import _shtab as shtab 15 | 16 | NAME = NAME.replace("_", "-") 17 | VERSION = rf"""{NAME} {__version__} 18 | Copyright (C) {datetime.now().year} 19 | Written by Wu, Zhenyu 20 | """ 21 | EPILOG = """ 22 | Report bugs to . 23 | """ 24 | 25 | 26 | def get_parser() -> ArgumentParser: 27 | r"""Get a parser for unit test.""" 28 | parser = ArgumentParser( 29 | epilog=EPILOG, 30 | formatter_class=RawDescriptionHelpFormatter, 31 | ) 32 | shtab.add_argument_to(parser) 33 | parser.add_argument("--version", version=VERSION, action="version") 34 | parser.add_argument( 35 | "--generate-schema", 36 | choices=FILETYPE.__args__, # type: ignore 37 | help="generate schema in an output format", 38 | ) 39 | parser.add_argument( 40 | "--output-format", 41 | choices=["json", "yaml", "toml"], 42 | default="json", 43 | help="output format: %(default)s", 44 | ) 45 | parser.add_argument( 46 | "--indent", 47 | type=int, 48 | default=2, 49 | help="generated json, yaml's indent, ignored by toml: %(default)s", 50 | ) 51 | parser.add_argument( 52 | "--color", 53 | choices=["auto", "always", "never"], 54 | default="auto", 55 | help="when to display color, default: %(default)s", 56 | ) 57 | parser.add_argument( 58 | "--check", 59 | nargs="*", 60 | default={}, 61 | help="check file's errors and warnings", 62 | ).complete = shtab.FILE # type: ignore 63 | parser.add_argument( 64 | "--convert", 65 | nargs="*", 66 | default={}, 67 | help="convert files to output format", 68 | ).complete = shtab.FILE # type: ignore 69 | return parser 70 | 71 | 72 | def main() -> None: 73 | r"""Parse arguments and provide shell completions.""" 74 | args = get_parser().parse_args() 75 | 76 | if args.generate_schema or args.check or args.convert: 77 | from lsp_tree_sitter.diagnose import check 78 | from lsp_tree_sitter.utils import pprint 79 | 80 | from .finders import DIAGNOSTICS_FINDER_CLASSES 81 | from .schema import MuttTrie 82 | from .utils import parser 83 | 84 | if args.generate_schema: 85 | from .misc import get_schema 86 | 87 | kwargs = ( 88 | {"indent": args.indent} if args.output_format != "toml" else {} 89 | ) 90 | pprint( 91 | get_schema(args.generate_schema), 92 | filetype=args.output_format, 93 | **kwargs, 94 | ) 95 | for file in args.convert: 96 | pprint( 97 | MuttTrie.from_file(file, parser.parse).to_json(), 98 | filetype=args.output_format, 99 | indent=args.indent, 100 | ) 101 | exit( 102 | check( 103 | args.check, 104 | parser.parse, 105 | DIAGNOSTICS_FINDER_CLASSES, 106 | None, 107 | args.color, 108 | ) 109 | ) 110 | 111 | from .server import MuttLanguageServer 112 | 113 | MuttLanguageServer(NAME, __version__).start_io() 114 | 115 | 116 | if __name__ == "__main__": 117 | main() 118 | -------------------------------------------------------------------------------- /src/mutt_language_server/schema.py: -------------------------------------------------------------------------------- 1 | r"""Schema 2 | ========== 3 | """ 4 | 5 | from dataclasses import dataclass 6 | 7 | from lsp_tree_sitter import UNI 8 | from lsp_tree_sitter.schema import Trie 9 | from lsprotocol.types import Position, Range 10 | from tree_sitter import Node 11 | 12 | DIRECTIVES = { 13 | "set_directive", 14 | "source_directive", 15 | } 16 | 17 | 18 | @dataclass 19 | class MuttTrie(Trie): 20 | r"""Mutt Trie.""" 21 | 22 | value: dict[str, "Trie"] | list["Trie"] | str | int | bool | None 23 | 24 | @classmethod 25 | def from_node(cls, node: Node, parent: "Trie | None") -> "Trie": 26 | r"""From node. 27 | 28 | :param node: 29 | :type node: Node 30 | :param parent: 31 | :type parent: Trie | None 32 | :rtype: "Trie" 33 | """ 34 | if node.type == "option": 35 | text = UNI(node).text 36 | if text.startswith("no"): 37 | return cls(UNI(node).range, parent, "no") 38 | else: 39 | return cls(UNI(node).range, parent, "yes") 40 | if node.type == "int": 41 | return cls(UNI(node).range, parent, int(UNI(node).text)) 42 | if node.type == "\n": 43 | return cls(UNI(node).range, parent, "") 44 | if node.type in {"shell", "string", "quadoption"}: 45 | return cls(UNI(node).range, parent, UNI(node).text) 46 | if node.type == "file": 47 | trie = cls(Range(Position(0, 0), Position(1, 0)), parent, {}) 48 | for child in node.children: 49 | if child.type not in DIRECTIVES: 50 | continue 51 | # directive name 52 | _type = child.type.split("_directive")[0] 53 | # add directive name to trie.value if it doesn't exist 54 | _value: dict[str, Trie] = trie.value # type: ignore 55 | if _type not in _value: 56 | trie.value[_type] = cls( # type: ignore 57 | UNI(child).range, 58 | trie, 59 | {} if _type != "source" else [], 60 | ) 61 | # the dictionary's key corresponding to directive name 62 | subtrie: Trie = trie.value[_type] # type: ignore 63 | # currently, only support set and source 64 | # set is a dict, source is a list 65 | value: dict[str, Trie] | list[Trie] = subtrie.value # type: ignore 66 | # fill subtrie.value 67 | if child.type == "set_directive": 68 | value: dict[str, Trie] 69 | is_assign = False 70 | for grandchild in child.children[1:]: 71 | if grandchild.type == "=": 72 | is_assign = True 73 | break 74 | # set option = value 75 | if is_assign: 76 | items = [] 77 | for grandchild in child.children[1:]: 78 | if grandchild.type in { 79 | " ", 80 | "=", 81 | "+=", 82 | "-=", 83 | '"', 84 | "'", 85 | "`", 86 | }: 87 | continue 88 | items += [grandchild] 89 | for k, v in zip(items[::2], items[1::2], strict=False): 90 | value[UNI(k).text] = cls.from_node(v, subtrie) 91 | # set option nooption invoption & option ? option 92 | else: 93 | for grandchild in child.children[1:]: 94 | if grandchild.type in {"&", "?", " "}: 95 | continue 96 | text = UNI(grandchild).text 97 | if text.startswith("no"): 98 | # generate trie from option node 99 | value[text.split("no")[-1]] = cls.from_node( 100 | grandchild, subtrie 101 | ) 102 | elif text.startswith("inv"): 103 | value[text.split("inv")[-1]] = cls.from_node( 104 | grandchild, subtrie 105 | ) 106 | else: 107 | value[text] = cls.from_node( 108 | grandchild, subtrie 109 | ) 110 | elif child.type == "source_directive": 111 | value += [ # type: ignore 112 | cls( 113 | UNI(child.children[1]).range, 114 | subtrie, 115 | UNI(child.children[1]).text, 116 | ) 117 | ] 118 | return trie 119 | raise NotImplementedError(node.type) 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mutt-language-server 2 | 3 | [![readthedocs](https://shields.io/readthedocs/mutt-language-server)](https://mutt-language-server.readthedocs.io) 4 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/neomutt/mutt-language-server/main.svg)](https://results.pre-commit.ci/latest/github/neomutt/mutt-language-server/main) 5 | [![github/workflow](https://github.com/neomutt/mutt-language-server/actions/workflows/main.yml/badge.svg)](https://github.com/neomutt/mutt-language-server/actions) 6 | [![codecov](https://codecov.io/gh/neomutt/mutt-language-server/branch/main/graph/badge.svg)](https://codecov.io/gh/neomutt/mutt-language-server) 7 | [![DeepSource](https://deepsource.io/gh/neomutt/mutt-language-server.svg/?show_trend=true)](https://deepsource.io/gh/neomutt/mutt-language-server) 8 | 9 | [![github/downloads](https://shields.io/github/downloads/neomutt/mutt-language-server/total)](https://github.com/neomutt/mutt-language-server/releases) 10 | [![github/downloads/latest](https://shields.io/github/downloads/neomutt/mutt-language-server/latest/total)](https://github.com/neomutt/mutt-language-server/releases/latest) 11 | [![github/issues](https://shields.io/github/issues/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/issues) 12 | [![github/issues-closed](https://shields.io/github/issues-closed/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/issues?q=is%3Aissue+is%3Aclosed) 13 | [![github/issues-pr](https://shields.io/github/issues-pr/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/pulls) 14 | [![github/issues-pr-closed](https://shields.io/github/issues-pr-closed/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/pulls?q=is%3Apr+is%3Aclosed) 15 | [![github/discussions](https://shields.io/github/discussions/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/discussions) 16 | [![github/milestones](https://shields.io/github/milestones/all/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/milestones) 17 | [![github/forks](https://shields.io/github/forks/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/network/members) 18 | [![github/stars](https://shields.io/github/stars/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/stargazers) 19 | [![github/watchers](https://shields.io/github/watchers/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/watchers) 20 | [![github/contributors](https://shields.io/github/contributors/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/graphs/contributors) 21 | [![github/commit-activity](https://shields.io/github/commit-activity/w/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/graphs/commit-activity) 22 | [![github/last-commit](https://shields.io/github/last-commit/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/commits) 23 | [![github/release-date](https://shields.io/github/release-date/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/releases/latest) 24 | 25 | [![github/license](https://shields.io/github/license/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server/blob/main/LICENSE) 26 | [![github/languages](https://shields.io/github/languages/count/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 27 | [![github/languages/top](https://shields.io/github/languages/top/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 28 | [![github/directory-file-count](https://shields.io/github/directory-file-count/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 29 | [![github/code-size](https://shields.io/github/languages/code-size/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 30 | [![github/repo-size](https://shields.io/github/repo-size/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 31 | [![github/v](https://shields.io/github/v/release/neomutt/mutt-language-server)](https://github.com/neomutt/mutt-language-server) 32 | 33 | [![pypi/status](https://shields.io/pypi/status/mutt-language-server)](https://pypi.org/project/mutt-language-server/#description) 34 | [![pypi/v](https://shields.io/pypi/v/mutt-language-server)](https://pypi.org/project/mutt-language-server/#history) 35 | [![pypi/downloads](https://shields.io/pypi/dd/mutt-language-server)](https://pypi.org/project/mutt-language-server/#files) 36 | [![pypi/format](https://shields.io/pypi/format/mutt-language-server)](https://pypi.org/project/mutt-language-server/#files) 37 | [![pypi/implementation](https://shields.io/pypi/implementation/mutt-language-server)](https://pypi.org/project/mutt-language-server/#files) 38 | [![pypi/pyversions](https://shields.io/pypi/pyversions/mutt-language-server)](https://pypi.org/project/mutt-language-server/#files) 39 | 40 | A language server for (neo)mutt's muttrc. 41 | 42 | - [x] [Diagnostic](https://microsoft.github.io/language-server-protocol/specifications/specification-current#diagnostic) 43 | - [x] [Document Link](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_documentLink) 44 | - [x] [Hover](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_hover) 45 | - [x] [Completion](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_completion) 46 | 47 | A screencast authored by @rbmarliere: 48 | 49 | [![asciicast](https://camo.githubusercontent.com/aa2be3ad710e855b3e6b7cd55f5261ac7582f1e69c8947f4619ba4c96f8cc506/68747470733a2f2f61736369696e656d612e6f72672f612f3631303634352e737667)](https://asciinema.org/a/610645) 50 | 51 | ![diagnostic](https://github.com/neomutt/mutt-language-server/assets/32936898/61c81c34-c5ae-4d66-82b2-2be5affb1162) 52 | 53 | ![document link](https://github.com/neomutt/mutt-language-server/assets/32936898/7db39120-401e-4be7-82c4-827609ab7f26) 54 | 55 | See 56 | [![readthedocs](https://shields.io/readthedocs/mutt-language-server)](https://mutt-language-server.readthedocs.io) 57 | to know more. 58 | 59 | ## How Does It Work 60 | 61 | See [lsp-tree-sitter](https://github.com/neomutt/lsp-tree-sitter#usage). 62 | 63 | ## Related Projects 64 | 65 | - [neomutt.vim](https://github.com/neomutt/neomutt.vim): vim filetype plugin 66 | for neomuttrc 67 | -------------------------------------------------------------------------------- /src/mutt_language_server/server.py: -------------------------------------------------------------------------------- 1 | r"""Server 2 | ========== 3 | """ 4 | 5 | from typing import Any 6 | 7 | from lsp_tree_sitter.diagnose import get_diagnostics 8 | from lsp_tree_sitter.finders import PositionFinder 9 | from lsprotocol.types import ( 10 | TEXT_DOCUMENT_COMPLETION, 11 | TEXT_DOCUMENT_DID_CHANGE, 12 | TEXT_DOCUMENT_DID_OPEN, 13 | TEXT_DOCUMENT_DOCUMENT_LINK, 14 | TEXT_DOCUMENT_HOVER, 15 | CompletionItem, 16 | CompletionItemKind, 17 | CompletionList, 18 | CompletionParams, 19 | DidChangeTextDocumentParams, 20 | DocumentLink, 21 | DocumentLinkParams, 22 | Hover, 23 | MarkupContent, 24 | MarkupKind, 25 | PublishDiagnosticsParams, 26 | TextDocumentPositionParams, 27 | ) 28 | from pygls.lsp.server import LanguageServer 29 | 30 | from .finders import DIAGNOSTICS_FINDER_CLASSES, ImportMuttFinder 31 | from .utils import get_schema, parser 32 | 33 | 34 | class MuttLanguageServer(LanguageServer): 35 | r"""Mutt language server.""" 36 | 37 | def __init__(self, *args: Any, **kwargs: Any) -> None: 38 | r"""Init. 39 | 40 | :param args: 41 | :type args: Any 42 | :param kwargs: 43 | :type kwargs: Any 44 | :rtype: None 45 | """ 46 | super().__init__(*args, **kwargs) 47 | self.trees = {} 48 | 49 | @self.feature(TEXT_DOCUMENT_DID_OPEN) 50 | @self.feature(TEXT_DOCUMENT_DID_CHANGE) 51 | def did_change(params: DidChangeTextDocumentParams) -> None: 52 | r"""Did change. 53 | 54 | :param params: 55 | :type params: DidChangeTextDocumentParams 56 | :rtype: None 57 | """ 58 | document = self.workspace.get_text_document( 59 | params.text_document.uri 60 | ) 61 | self.trees[document.uri] = parser.parse(document.source.encode()) 62 | diagnostics = get_diagnostics( 63 | document.uri, 64 | self.trees[document.uri], 65 | DIAGNOSTICS_FINDER_CLASSES, 66 | "muttrc", 67 | ) 68 | self.text_document_publish_diagnostics( 69 | PublishDiagnosticsParams( 70 | params.text_document.uri, 71 | diagnostics, 72 | ) 73 | ) 74 | 75 | @self.feature(TEXT_DOCUMENT_DOCUMENT_LINK) 76 | def document_link(params: DocumentLinkParams) -> list[DocumentLink]: 77 | r"""Get document links. 78 | 79 | :param params: 80 | :type params: DocumentLinkParams 81 | :rtype: list[DocumentLink] 82 | """ 83 | document = self.workspace.get_text_document( 84 | params.text_document.uri 85 | ) 86 | return ImportMuttFinder().get_document_links( 87 | document.uri, self.trees[document.uri] 88 | ) 89 | 90 | @self.feature(TEXT_DOCUMENT_HOVER) 91 | def hover(params: TextDocumentPositionParams) -> Hover | None: 92 | r"""Hover. 93 | 94 | :param params: 95 | :type params: TextDocumentPositionParams 96 | :rtype: Hover | None 97 | """ 98 | document = self.workspace.get_text_document( 99 | params.text_document.uri 100 | ) 101 | uni = PositionFinder(params.position).find( 102 | document.uri, self.trees[document.uri] 103 | ) 104 | if uni is None: 105 | return None 106 | text = uni.text 107 | result = None 108 | if uni.node.range.start_point[1] == 0: 109 | result = get_schema()["properties"].get(text) 110 | elif uni.node.type == "option": 111 | result = get_schema()["properties"]["set"]["properties"].get( 112 | text 113 | ) 114 | if result is None: 115 | return None 116 | return Hover( 117 | MarkupContent(MarkupKind.Markdown, result["description"]), 118 | uni.range, 119 | ) 120 | 121 | @self.feature(TEXT_DOCUMENT_COMPLETION) 122 | def completions(params: CompletionParams) -> CompletionList: 123 | r"""Completions. 124 | 125 | :param params: 126 | :type params: CompletionParams 127 | :rtype: CompletionList 128 | """ 129 | document = self.workspace.get_text_document( 130 | params.text_document.uri 131 | ) 132 | uni = PositionFinder(params.position, right_equal=True).find( 133 | document.uri, self.trees[document.uri] 134 | ) 135 | if uni is None: 136 | return CompletionList(False, []) 137 | text = uni.text 138 | if uni.node.range.start_point[1] == 0: 139 | return CompletionList( 140 | False, 141 | [ 142 | CompletionItem( 143 | x, 144 | kind=CompletionItemKind.Keyword, 145 | documentation=MarkupContent( 146 | MarkupKind.Markdown, property["description"] 147 | ), 148 | insert_text=x, 149 | ) 150 | for x, property in get_schema()["properties"].items() 151 | if x.startswith(text) 152 | ], 153 | ) 154 | elif uni.node.type == "option": 155 | return CompletionList( 156 | False, 157 | [ 158 | CompletionItem( 159 | x, 160 | kind=CompletionItemKind.Variable, 161 | documentation=MarkupContent( 162 | MarkupKind.Markdown, property["description"] 163 | ), 164 | insert_text=x, 165 | ) 166 | for x, property in get_schema()["properties"]["set"][ 167 | "properties" 168 | ].items() 169 | if x.startswith(text) 170 | ], 171 | ) 172 | return CompletionList(False, []) 173 | -------------------------------------------------------------------------------- /src/mutt_language_server/misc/__init__.py: -------------------------------------------------------------------------------- 1 | r"""Misc 2 | ======== 3 | """ 4 | 5 | import re 6 | from typing import Any 7 | 8 | from lsp_tree_sitter.misc import get_md_tokens 9 | 10 | from .. import FILETYPE 11 | from .._metainfo import SOURCE, project 12 | 13 | 14 | def get_schema(filetype: FILETYPE = "neomuttrc") -> dict[str, Any]: 15 | r"""Get schema. 16 | 17 | :rtype: dict[str, Any] 18 | """ 19 | filetype = "neomuttrc" 20 | schema = { 21 | "$id": ( 22 | f"{SOURCE}/blob/main/src/" 23 | f"termux_language_server/assets/json/{filetype}.json" 24 | ), 25 | "$schema": "http://json-schema.org/draft-07/schema#", 26 | "$comment": ( 27 | "Don't edit this file directly! It is generated by " 28 | f"`{project} --generate-schema={filetype}`." 29 | ), 30 | "type": "object", 31 | "properties": {}, 32 | } 33 | tokens = get_md_tokens("neomuttrc") 34 | indices = [] 35 | end_index = len(tokens) 36 | for i, token in enumerate(tokens): 37 | if token.content == "PATTERNS": 38 | end_index = i 39 | break 40 | if ( 41 | token.type == "code_block" 42 | and token.content.islower() 43 | or token.type == "inline" 44 | and token.content.startswith("**") 45 | and token.content.endswith("*") 46 | ): 47 | indices += [i] 48 | for i, index in enumerate(indices): 49 | keywords = [ 50 | line.split()[0].strip("*") 51 | for line in tokens[index].content.splitlines() 52 | ] 53 | for keyword in keywords: 54 | schema["properties"][keyword] = { 55 | "description": f"""```neomuttrc 56 | {tokens[index].content.strip()} 57 | ``` 58 | """ 59 | } 60 | index2 = end_index if len(indices) - 1 == i else indices[i + 1] 61 | for token in tokens[index + 1 : index2]: 62 | if token.content != "" and not token.content.startswith("