2024-08-13

Creating Your Own Language Server Protocol (LSP) for Vim with Python's PyGLS

In the ever-evolving landscape of software development, the Language Server Protocol (LSP) has emerged as a game-changer, enabling developers to enhance their coding experience with powerful features like autocompletion, error checking, and more. If you're a Vim enthusiast looking to supercharge your editor with custom language support, you're in the right place. In this blog post, I'll walk you through the process of creating your own LSP using the Python pygls library.

GNU diction command is a Unix utility designed to analyze a text file against a predefined set of rules, helping writers enhance their writing style by pinpointing potential issues. Rules are stored in an external text file as a two-column table, named with the corresponding language code (e.g., 'en' for English).

As shown in the video below, we will create a simple LSP in Python that integrates with VIM, and which is capable to provide real-time suggestions from diction's rules text files.

First step is setting up a virtual environment using my handy vox PowerShell script, ensuring that our project dependencies are neatly contained. Then, the pygls (pronounced like pie glass) library can be installed with:

  pip install pygls

which provides the tools required to build our language server.

The entire language server is less than 70 lines of code python script, shown below:

"""

Copyright (c) 2024 S. Tessarin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

1. The above copyright notice and this permission notice shall be included in
   all copies or substantial portions of the Software.

2. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
"""
    import logging
    from pygls.server import LanguageServer
    from lsprotocol import types
    import re

    def read_dict(file_path):
        result_dict = {}
        with open(file_path, 'r') as file:
            for line in file:
                # Skip comments
                if line.startswith('#'):
                    continue
                if line:
                    parts = line.strip().split('\t')
                    if len(parts) >= 2:
                        key = parts[0].strip()
                        value = parts[1].strip()
                        result_dict[key] = value
        return result_dict

    server = LanguageServer("diction-lsp-server", "v0.1")


    @server.feature(types.TEXT_DOCUMENT_HOVER)
    def hover(ls: LanguageServer, params: types.HoverParams):
        pos = params.position
        document_uri = params.text_document.uri
        document = ls.workspace.get_text_document(document_uri)

        try:
            line = document.lines[pos.line]
        except IndexError:
            return None

        for word in re.findall(r'(?<!\w)[*]*([\w]+)[*]*(?!\w)',line):
            try:
                value = f"{word}: {server.dict[word]}"
                break
            except KeyError:
                pass
        else:
            return None
        hover_content = [ "\n",
                value,
                 "\n"]

        return types.Hover(
            contents=types.MarkupContent(
                kind=types.MarkupKind.Markdown,
                value="\n".join(hover_content),
            ),
            range=types.Range(
                start = types.Position(line=pos.line, character=0),
                end = types.Position(line=pos.line + 1, character=0),
            ),
        )

    if __name__ == "__main__":
        logging.basicConfig(filename='lsp.log',level=logging.INFO, format="%(message)s")
        dictionary = read_dict("en")
        words_list = list(dictionary.keys())
        server.words_list =  words_list
        server.dict = dictionary
        server.start_io()

To ensure that Vim works seamlessly with our newly created LSP, you need to install the popular yegappan/lsp plugin.

The configuration provided below initializes the LSP for MediaWiki, Markdown, and Typst text files. Please note that it is limited to the directory containing the LSP server (lsp.py) and diction's rules file.

                \#{
                \    name: 'pygls',
                \    filetype: ['mediawiki','markdown','typst'],
                \    path: 'py',
                \    args: ['lsp.py'],
                \ },

To change the background color of the pop-up, you can add the following option to your syntax file or .vimrc:

   highlight Pmenu guibg=darkblue