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):
|
class Object(AstNode):
|
||||||
@validate("namespace", "class_name")
|
@validate("namespace")
|
||||||
def gir_class_exists(self):
|
def gir_ns_exists(self):
|
||||||
if not self.tokens["ignore_gir"]:
|
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"])
|
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
|
@property
|
||||||
def gir_class(self):
|
def gir_class(self):
|
||||||
if not self.tokens["ignore_gir"]:
|
if not self.tokens["ignore_gir"]:
|
||||||
|
@ -180,7 +190,8 @@ class Object(AstNode):
|
||||||
|
|
||||||
@docs("namespace")
|
@docs("namespace")
|
||||||
def namespace_docs(self):
|
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")
|
@docs("class_name")
|
||||||
|
@ -398,17 +409,17 @@ class IdentValue(Value):
|
||||||
if self.tokens["value"] not in type.members:
|
if self.tokens["value"] not in type.members:
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
f"{self.tokens['value']} is not a member of {type.full_name}",
|
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):
|
elif isinstance(type, gir.BoolType):
|
||||||
# would have been parsed as a LiteralValue if it was correct
|
# would have been parsed as a LiteralValue if it was correct
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
f"Expected 'true' or 'false' for boolean value",
|
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"])
|
object = self.root.objects_by_id.get(self.tokens["value"])
|
||||||
if object is None:
|
if object is None:
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: LGPL-3.0-or-later
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import typing as T
|
import typing as T
|
||||||
import sys, traceback
|
import sys, traceback
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -45,13 +45,14 @@ class CompileError(PrintableError):
|
||||||
|
|
||||||
category = "error"
|
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)
|
super().__init__(message)
|
||||||
|
|
||||||
self.message = message
|
self.message = message
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
self.hints = hints or []
|
self.hints = hints or []
|
||||||
|
self.actions = actions or []
|
||||||
|
|
||||||
if did_you_mean is not None:
|
if did_you_mean is not None:
|
||||||
self._did_you_mean(*did_you_mean)
|
self._did_you_mean(*did_you_mean)
|
||||||
|
@ -72,6 +73,7 @@ class CompileError(PrintableError):
|
||||||
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
|
self.hint(f"Did you mean `{recommend}` (note the capitalization)?")
|
||||||
else:
|
else:
|
||||||
self.hint(f"Did you mean `{recommend}`?")
|
self.hint(f"Did you mean `{recommend}`?")
|
||||||
|
self.actions.append(CodeAction(f"Change to `{recommend}`", recommend))
|
||||||
else:
|
else:
|
||||||
self.hint("Did you check your spelling?")
|
self.hint("Did you check your spelling?")
|
||||||
self.hint("Are your dependencies up to date?")
|
self.hint("Are your dependencies up to date?")
|
||||||
|
@ -92,6 +94,12 @@ at {filename} line {line_num} column {col_num}:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CodeAction:
|
||||||
|
title: str
|
||||||
|
replace_with: str
|
||||||
|
|
||||||
|
|
||||||
class AlreadyCaughtError(Exception):
|
class AlreadyCaughtError(Exception):
|
||||||
""" Emitted when a validation has already failed and its error message
|
""" Emitted when a validation has already failed and its error message
|
||||||
should not be repeated. """
|
should not be repeated. """
|
||||||
|
|
|
@ -388,9 +388,9 @@ class GirContext:
|
||||||
return None
|
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
|
""" 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"
|
ns = ns or "Gtk"
|
||||||
|
|
||||||
|
@ -400,6 +400,14 @@ class GirContext:
|
||||||
did_you_mean=(ns, self.namespaces.keys()),
|
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)
|
type = self.get_type(name, ns)
|
||||||
|
|
||||||
if type is None:
|
if type is None:
|
||||||
|
|
|
@ -164,6 +164,7 @@ class LanguageServer:
|
||||||
"full": True,
|
"full": True,
|
||||||
},
|
},
|
||||||
"completionProvider": {},
|
"completionProvider": {},
|
||||||
|
"codeActionProvider": {},
|
||||||
"hoverProvider": True,
|
"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):
|
def _send_file_updates(self, open_file: OpenFile):
|
||||||
self._send_notification("textDocument/publishDiagnostics", {
|
self._send_notification("textDocument/publishDiagnostics", {
|
||||||
"uri": open_file.uri,
|
"uri": open_file.uri,
|
||||||
|
@ -233,13 +263,8 @@ class LanguageServer:
|
||||||
})
|
})
|
||||||
|
|
||||||
def _create_diagnostic(self, text, err):
|
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 {
|
return {
|
||||||
"range": {
|
"range": utils.idxs_to_range(err.start, err.end, text),
|
||||||
"start": { "line": start_l, "character": start_c },
|
|
||||||
"end": { "line": end_l, "character": end_c },
|
|
||||||
},
|
|
||||||
"message": err.message,
|
"message": err.message,
|
||||||
"severity": 1,
|
"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:
|
def pos_to_idx(line: int, col: int, text: str) -> int:
|
||||||
lines = text.splitlines(keepends=True)
|
lines = text.splitlines(keepends=True)
|
||||||
return sum([len(line) for line in lines[:line]]) + col
|
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