@tool class_name GDLinter extends EditorPlugin const DockScene := preload("res://addons/gdLinter/UI/Dock.tscn") var icon_error := EditorInterface.get_editor_theme().get_icon("Error", "EditorIcons") var color_error: Color = EditorInterface.get_editor_settings()\ .get_setting("text_editor/theme/highlighting/comment_markers/critical_color") var icon_error_ignore := EditorInterface.get_editor_theme().get_icon("ErrorWarning", "EditorIcons") var icon_ignore := EditorInterface.get_editor_theme().get_icon("Warning", "EditorIcons") var icon_success := EditorInterface.get_editor_theme().get_icon("StatusSuccess", "EditorIcons") var color_success: Color = EditorInterface.get_editor_settings()\ .get_setting("text_editor/theme/highlighting/comment_markers/notice_color") var bottom_panel_button: Button var highlight_lines: PackedInt32Array var item_lists: Array[ItemList] var script_editor: ScriptEditor var _dock_ui: GDLinterDock var _is_gdlint_installed: bool var _ignore: Resource var _gdlint_path: String func _enter_tree() -> void: # install the GDLint dock _dock_ui = DockScene.instantiate() _dock_ui.gd_linter = self bottom_panel_button = add_control_to_bottom_panel(_dock_ui, "GDLint") # connect signal to lint on save resource_saved.connect(on_resource_saved) script_editor = EditorInterface.get_script_editor() script_editor.editor_script_changed.connect(_on_editor_script_changed) _gdlint_path = get_gdlint_path() get_gdlint_version() prints("Loading GDLint Plugin success") # Dunno how highlighting lines in Godot works, since it get removed after a second or so # So I use this evil workaround straight from hell: #func _process(_delta: float) -> void: #if not get_current_editor(): #return # #if not highlight_lines.is_empty(): #set_line_color(color_error) func _on_editor_script_changed(script: Script) -> void: _dock_ui.clear_items() on_resource_saved(script) func get_gdlint_version() -> void: var output := [] OS.execute(_gdlint_path, ["--version"], output) _is_gdlint_installed = true if not output[0].is_empty() else false if _is_gdlint_installed: _dock_ui.version.text = "Using %s" % output[0] else: _dock_ui.version.text = "gdlint not found!" func _exit_tree() -> void: if is_instance_valid(_dock_ui): remove_control_from_bottom_panel(_dock_ui) _dock_ui.free() if Engine.get_version_info().hex >= 0x40201: prints("Unload GDLint Plugin success") func on_resource_saved(resource: Resource) -> void: if not resource is GDScript: return _dock_ui.clear_items() clear_highlights() # Show resource path in the GDLint Dock _dock_ui.file.text = resource.resource_path # Execute linting and get its output var filepath: String = ProjectSettings.globalize_path(resource.resource_path) var gdlint_output: Array = [] var output_array: PackedStringArray var exit_code = OS.execute(_gdlint_path, [filepath], gdlint_output, true) if not exit_code == -1: var output_string: String = gdlint_output[0] output_array = output_string.replace(filepath+":", "Line ").split("\n") _dock_ui.set_problems_label(_dock_ui.num_problems) _dock_ui.set_ignored_problems_label(_dock_ui.num_ignored_problems) # Workaround until unique name bug is fixed # https://github.com/Scony/godot-gdscript-toolkit/issues/284 # Hope I won't break other stuff with it if not output_array.size() or output_array[0] == "Line ": printerr("gdLint Error: ", output_array, "\n File can't be linted!") return # When there is no error if output_array.size() <= 2: bottom_panel_button.add_theme_constant_override(&"icon_max_width", 8) bottom_panel_button.icon = icon_success return # When errors are found create buttons in the dock for i in output_array.size()-2: var regex := RegEx.new() regex.compile("\\d+") var result := regex.search(output_array[i]) if result: var current_line := int(result.strings[0])-1 var error := output_array[i].rsplit(":", true, 1) if len(error) > 1: _dock_ui.create_item(current_line+1, error[1]) if _dock_ui.is_error_ignored(error[1]): continue highlight_lines.append(current_line) _dock_ui.set_problems_label(_dock_ui.num_problems) _dock_ui.set_ignored_problems_label(_dock_ui.num_ignored_problems) # Error, no Ignore if _dock_ui.num_problems > 0 and _dock_ui.num_ignored_problems <= 0: bottom_panel_button.icon = icon_error # no Error, Ignore elif _dock_ui.num_problems <= 0 and _dock_ui.num_ignored_problems > 0: bottom_panel_button.icon = icon_ignore # Error, Ignore elif _dock_ui.num_problems > 0 and _dock_ui.num_ignored_problems > 0: bottom_panel_button.icon = icon_error_ignore else: bottom_panel_button.icon = null _dock_ui.script_text_editor = EditorInterface.get_script_editor().get_current_editor() func set_line_color(color: Color) -> void: var current_code_editor := get_current_editor() if current_code_editor == null: return for line: int in highlight_lines: # Skip line if this one is from the old code editor if line > current_code_editor.get_line_count()-1: continue current_code_editor.set_line_background_color(line, color.darkened(0.5)) func clear_highlights() -> void: set_line_color(Color(0, 0, 0, 0)) highlight_lines.clear() func get_current_editor() -> CodeEdit: var current_editor := EditorInterface.get_script_editor().get_current_editor() if current_editor == null: return return current_editor.get_base_editor() as CodeEdit func get_gdlint_path() -> String: if OS.get_name() == "Windows": return "gdlint" # macOS & Linux var output := [] OS.execute("python3", ["-m", "site", "--user-base"], output) var python_bin_folder := (output[0] as String).strip_edges().path_join("bin") if FileAccess.file_exists(python_bin_folder.path_join("gdlint")): return python_bin_folder.path_join("gdlint") # Linux dirty hardcoded fallback if OS.get_name() == "Linux": if FileAccess.file_exists("/usr/bin/gdlint"): return "/usr/bin/gdlint" # Global fallback return "gdlint"