pappasam / jedi-language-server

A Python language server exclusively for Jedi. If Jedi supports it well, this language server should too.
MIT License
596 stars 45 forks source link

publishDiagnostics after #187 #241

Closed pyscripter closed 1 year ago

pyscripter commented 1 year ago

Proposal

With the resolution of #187 and the related issues with JEDI we are left with no reliable way to get syntax errors published. I suggest the replacement of that functionality with the use of the compile function.

Suggested changes:

Add to jedi_utils:

from ast import PyCF_ONLY_AST

def lsp_python_diagnostic(uri: str, source: str) -> Diagnostic:
    """Get LSP Diagnostic using the compile builtin."""
    try:
        compile(source, uri, "exec", PyCF_ONLY_AST)
        return None
    except SyntaxError as err:
        column, line = err.offset - 1, err.lineno - 1
        until_column = getattr(err, "end_offset", 0) - 1
        until_line = getattr(err, "end_lineno", 0) - 1

        if (line, column) >= (until_line, until_column):
            until_column, until_line = column, line
            column = 0

        return Diagnostic(
            range=Range(
                start=Position(line=line, character=column),
                end=Position(line=until_line, character=until_column),
            ),
            message=err.__class__.__name__ + ': ' + str(err),
            severity=DiagnosticSeverity.Error,
            source="compile",
        )

Change _publish_diagnostics in server.py to:

def _publish_diagnostics(server: JediLanguageServer, uri: str) -> None:
    """Helper function to publish diagnostics for a file."""
    # since this function is executed with a delay we need to check
    # whether the document still exists
    if not (uri in server.workspace.documents):
        return

    doc = server.workspace.get_document(uri)
    diagnostic = jedi_utils.lsp_python_diagnostic(uri, doc.source)
    diagnostics = [diagnostic] if diagnostic else []

    server.publish_diagnostics(uri, diagnostics)

This is all that it takes. It is reliable and probably faster than using jedi. The only downside is that it provides just the first error even when there are many. Still quite useful.

Further refinement

The idea and the following quite remarkable decorator function are borrowed from python_lsp_server.

Add to jedi_utils:

import functools
import threading
import inspect

def debounce(interval_s, keyed_by=None):
    """Debounce calls to this function until interval_s seconds have passed."""
    def wrapper(func):
        timers = {}
        lock = threading.Lock()

        @functools.wraps(func)
        def debounced(*args, **kwargs):
            sig = inspect.signature(func)
            call_args = sig.bind(*args, **kwargs)
            key = call_args.arguments[keyed_by] if keyed_by else None

            def run():
                with lock:
                    del timers[key]
                return func(*args, **kwargs)

            with lock:
                old_timer = timers.get(key)
                if old_timer:
                    old_timer.cancel()

                timer = threading.Timer(interval_s, run)
                timers[key] = timer
                timer.start()
        return debounced
    return wrapper

In server.py decorate _publish_diagnostics

@jedi_utils.debounce(1, keyed_by='uri')
def _publish_diagnostics(server: JediLanguageServer, uri: str) -> None:
    """Helper function to publish diagnostics for a file."""
    # the debounce decorator delays the execution by 1 second
    # canceling notifications that happen in that interval
    # since this function is executed with a delay we need to check
    # whether the document still exists
    if not (uri in server.workspace.documents):
        return

    doc = server.workspace.get_document(uri)
    diagnostic = jedi_utils.lsp_python_diagnostic(uri, doc.source)
    diagnostics = [diagnostic] if diagnostic else []

    server.publish_diagnostics(uri, diagnostics)

So, for example, if the user types fast, instead of sending multiple publishDiagnostics notifications only one will be send with a bit of delay. Each new one will cancel the previous one if it has not already been executed.

I can submit a PR if you approve the above.

pappasam commented 1 year ago

I'm definitely interested in all of the above. If you find the time, please submit a PR and I'll review / get it in a new release soon!

pyscripter commented 1 year ago

Submitted #242