mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
lsp: Add code actions
"Did you mean" hints are automatically converted into code actions.
This commit is contained in:
parent
d89f2356b4
commit
d511b3f1e3
5 changed files with 82 additions and 16 deletions
|
@ -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(
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue