diff --git a/gtkblueprinttool/ast.py b/gtkblueprinttool/ast.py index 2ef4960..4457045 100644 --- a/gtkblueprinttool/ast.py +++ b/gtkblueprinttool/ast.py @@ -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( diff --git a/gtkblueprinttool/errors.py b/gtkblueprinttool/errors.py index 599138e..41d4ce9 100644 --- a/gtkblueprinttool/errors.py +++ b/gtkblueprinttool/errors.py @@ -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. """ diff --git a/gtkblueprinttool/gir.py b/gtkblueprinttool/gir.py index dfde6ab..c59bba7 100644 --- a/gtkblueprinttool/gir.py +++ b/gtkblueprinttool/gir.py @@ -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: diff --git a/gtkblueprinttool/lsp.py b/gtkblueprinttool/lsp.py index 0c58f45..23fe63f 100644 --- a/gtkblueprinttool/lsp.py +++ b/gtkblueprinttool/lsp.py @@ -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, } diff --git a/gtkblueprinttool/utils.py b/gtkblueprinttool/utils.py index cf733c7..0954bc4 100644 --- a/gtkblueprinttool/utils.py +++ b/gtkblueprinttool/utils.py @@ -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, + }, + }