Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

publishDiagnostics after #187 #241

Closed
pyscripter opened this issue Dec 6, 2022 · 2 comments
Closed

publishDiagnostics after #187 #241

pyscripter opened this issue Dec 6, 2022 · 2 comments

Comments

@pyscripter
Copy link
Contributor

pyscripter commented Dec 6, 2022

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
Copy link
Owner

pappasam commented Dec 6, 2022

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
Copy link
Contributor Author

Submitted #242

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants