lsp: Add code actions

"Did you mean" hints are automatically converted into code actions.
This commit is contained in:
James Westman 2021-11-11 22:59:49 -06:00
parent d89f2356b4
commit d511b3f1e3
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
5 changed files with 82 additions and 16 deletions

View file

@ -167,11 +167,21 @@ class Template(AstNode):
class Object(AstNode):
@validate("namespace", "class_name")
def gir_class_exists(self):
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@validate("class_name")
def gir_class_exists(self):
if not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"])
@property
def gir_class(self):
if not self.tokens["ignore_gir"]:
@ -180,7 +190,8 @@ class Object(AstNode):
@docs("namespace")
def namespace_docs(self):
return self.root.gir.namespaces[self.tokens["namespace"]].doc
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
@ -398,17 +409,17 @@ class IdentValue(Value):
if self.tokens["value"] not in type.members:
raise CompileError(
f"{self.tokens['value']} is not a member of {type.full_name}",
did_you_mean=type.members.keys(),
did_you_mean=(self.tokens['value'], type.members.keys()),
)
elif isinstance(type, gir.BoolType):
# would have been parsed as a LiteralValue if it was correct
raise CompileError(
f"Expected 'true' or 'false' for boolean value",
did_you_mean=["true", "false"],
did_you_mean=(self.tokens['value'], ["true", "false"]),
)
else:
elif type is not None:
object = self.root.objects_by_id.get(self.tokens["value"])
if object is None:
raise CompileError(

View file

@ -17,7 +17,7 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from dataclasses import dataclass
import typing as T
import sys, traceback
from . import utils
@ -45,13 +45,14 @@ class CompileError(PrintableError):
category = "error"
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None):
def __init__(self, message, start=None, end=None, did_you_mean=None, hints=None, actions=None):
super().__init__(message)
self.message = message
self.start = start
self.end = end
self.hints = hints or []
self.actions = actions or []
if did_you_mean is not None:
self._did_you_mean(*did_you_mean)
@ -72,6 +73,7 @@ class CompileError(PrintableError):
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
else:
self.hint(f"Did you mean `{recommend}`?")
self.actions.append(CodeAction(f"Change to `{recommend}`", recommend))
else:
self.hint("Did you check your spelling?")
self.hint("Are your dependencies up to date?")
@ -92,6 +94,12 @@ at {filename} line {line_num} column {col_num}:
print()
@dataclass
class CodeAction:
title: str
replace_with: str
class AlreadyCaughtError(Exception):
""" Emitted when a validation has already failed and its error message
should not be repeated. """

View file

@ -388,9 +388,9 @@ class GirContext:
return None
def validate_class(self, name: str, ns: str):
def validate_ns(self, ns: str):
""" Raises an exception if there is a problem looking up the given
class (it doesn't exist, it isn't a class, etc.) """
namespace. """
ns = ns or "Gtk"
@ -400,6 +400,14 @@ class GirContext:
did_you_mean=(ns, self.namespaces.keys()),
)
def validate_class(self, name: str, ns: str):
""" Raises an exception if there is a problem looking up the given
class (it doesn't exist, it isn't a class, etc.) """
ns = ns or "Gtk"
self.validate_ns(ns)
type = self.get_type(name, ns)
if type is None:

View file

@ -164,6 +164,7 @@ class LanguageServer:
"full": True,
},
"completionProvider": {},
"codeActionProvider": {},
"hoverProvider": True,
}
})
@ -226,6 +227,35 @@ class LanguageServer:
})
@command("textDocument/codeAction")
def code_actions(self, id, params):
open_file = self._open_files[params["textDocument"]["uri"]]
range_start = utils.pos_to_idx(params["range"]["start"]["line"], params["range"]["start"]["character"], open_file.text)
range_end = utils.pos_to_idx(params["range"]["end"]["line"], params["range"]["end"]["character"], open_file.text)
actions = [
{
"title": action.title,
"kind": "quickfix",
"diagnostics": [self._create_diagnostic(open_file.text, diagnostic)],
"edit": {
"changes": {
open_file.uri: [{
"range": utils.idxs_to_range(diagnostic.start, diagnostic.end, open_file.text),
"newText": action.replace_with
}]
}
}
}
for diagnostic in open_file.diagnostics
if not (diagnostic.end < range_start or diagnostic.start > range_end)
for action in diagnostic.actions
]
self._send_response(id, actions)
def _send_file_updates(self, open_file: OpenFile):
self._send_notification("textDocument/publishDiagnostics", {
"uri": open_file.uri,
@ -233,13 +263,8 @@ class LanguageServer:
})
def _create_diagnostic(self, text, err):
start_l, start_c = utils.idx_to_pos(err.start, text)
end_l, end_c = utils.idx_to_pos(err.end or err.start, text)
return {
"range": {
"start": { "line": start_l, "character": start_c },
"end": { "line": end_l, "character": end_c },
},
"range": utils.idxs_to_range(err.start, err.end, text),
"message": err.message,
"severity": 1,
}

View file

@ -78,3 +78,17 @@ def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
def pos_to_idx(line: int, col: int, text: str) -> int:
lines = text.splitlines(keepends=True)
return sum([len(line) for line in lines[:line]]) + col
def idxs_to_range(start: int, end: int, text: str):
start_l, start_c = idx_to_pos(start, text)
end_l, end_c = idx_to_pos(end, text)
return {
"start": {
"line": start_l,
"character": start_c,
},
"end": {
"line": end_l,
"character": end_c,
},
}