From 62a866d9bbd8226bfdc9111746905b7622d68370 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sun, 29 Mar 2026 03:09:43 -0500 Subject: [PATCH] feat(tree-sitter, views): initialize AST on focus and emit source view creation event - Add set_ast helper to centralize Tree-sitter parsing logic - Parse and attach AST on FocusedViewEvent and TextChangedEvent - Request file from buffer on view focus before parsing - Fix parser guard condition in get_parser (handle missing language properly) - Emit CreatedSourceViewEvent when a new source view is added - Register CreatedSourceViewEvent in DTO exports - Update TODO: - Remove completed collapsible code blocks task - Add fix note for code block icon desync issue chore: - Add scaffolding for code_fold UI plugin - Add created_source_view_event DTO --- TODO.md | 2 +- .../code/event-watchers/tree_sitter/plugin.py | 38 ++++--- .../event-watchers/tree_sitter/tree_sitter.py | 2 +- plugins/code/ui/code_fold/__init__.py | 3 + plugins/code/ui/code_fold/__main__.py | 3 + plugins/code/ui/code_fold/fold_types.py | 42 ++++++++ plugins/code/ui/code_fold/folding_actions.py | 22 +++++ plugins/code/ui/code_fold/folding_engine.py | 31 ++++++ plugins/code/ui/code_fold/gutter_renderer.py | 93 ++++++++++++++++++ plugins/code/ui/code_fold/manifest.json | 7 ++ plugins/code/ui/code_fold/plugin.py | 98 +++++++++++++++++++ .../views/source_views_controller.py | 6 ++ src/libs/dto/code/events/__init__.py | 1 + .../code/events/created_source_view_event.py | 14 +++ 14 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 plugins/code/ui/code_fold/__init__.py create mode 100644 plugins/code/ui/code_fold/__main__.py create mode 100644 plugins/code/ui/code_fold/fold_types.py create mode 100644 plugins/code/ui/code_fold/folding_actions.py create mode 100644 plugins/code/ui/code_fold/folding_engine.py create mode 100644 plugins/code/ui/code_fold/gutter_renderer.py create mode 100644 plugins/code/ui/code_fold/manifest.json create mode 100644 plugins/code/ui/code_fold/plugin.py create mode 100644 src/libs/dto/code/events/created_source_view_event.py diff --git a/TODO.md b/TODO.md index b24fcb1..300d5d3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ ___ ### Add -1. Add Collapsable code blocks 1. Add Godot LSP Client 1. Add Terminal plugin 1. Add Plugin to | and | to split views up, down, left, right @@ -14,5 +13,6 @@ ___ ___ ### Fix - Fix on lsp client unload to close files lsp side and unload server endpoint +- Fix Collapsable code blocks icon desync on new/old lines or text cut/pasted ___ diff --git a/plugins/code/event-watchers/tree_sitter/plugin.py b/plugins/code/event-watchers/tree_sitter/plugin.py index 702c01d..47da79c 100644 --- a/plugins/code/event-watchers/tree_sitter/plugin.py +++ b/plugins/code/event-watchers/tree_sitter/plugin.py @@ -16,21 +16,37 @@ class Plugin(PluginCode): super(Plugin, self).__init__() + def set_ast(self, file): + if not hasattr(file, "tree_sitter"): + parser = get_parser( file.ftype ) + if not parser: return + + file.tree_sitter = parser + + buffer = file.buffer + start_itr, \ + end_itr = buffer.get_bounds() + text = buffer.get_text(start_itr, end_itr, True) + + tree = file.tree_sitter.parse( text.encode("UTF-8") ) + file.ast = tree + def _controller_message(self, event: Code_Event_Types.CodeEvent): - if isinstance(event, Code_Event_Types.TextChangedEvent): - if not hasattr(event.file, "tree_sitter"): - parser = get_parser( event.file.ftype ) - if not parser: return + if isinstance(event, Code_Event_Types.FocusedViewEvent): + self.view = event.view + event = Event_Factory.create_event( + "get_file", buffer = self.view.get_buffer() + ) + self.emit_to("files", event) - event.file.tree_sitter = parser + file = event.response - buffer = event.file.buffer - start_itr, \ - end_itr = buffer.get_bounds() - text = buffer.get_text(start_itr, end_itr, True) + if not file: return + if file.ftype == "buffer": return - tree = event.file.tree_sitter.parse( text.encode("UTF-8") ) - event.file.ast = tree + self.set_ast(file) + elif isinstance(event, Code_Event_Types.TextChangedEvent): + self.set_ast(event.file) # root = tree.root_node # print("Root type:", root.type) diff --git a/plugins/code/event-watchers/tree_sitter/tree_sitter.py b/plugins/code/event-watchers/tree_sitter/tree_sitter.py index 2903a77..1c51b13 100644 --- a/plugins/code/event-watchers/tree_sitter/tree_sitter.py +++ b/plugins/code/event-watchers/tree_sitter/tree_sitter.py @@ -48,7 +48,7 @@ def get_parser(lang_name: str) -> Parser | None: language = LANGUAGES[lang_name] - if not language in LANGUAGES: return + if not language: return parser = Parser() parser.language = language diff --git a/plugins/code/ui/code_fold/__init__.py b/plugins/code/ui/code_fold/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/code/ui/code_fold/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/code/ui/code_fold/__main__.py b/plugins/code/ui/code_fold/__main__.py new file mode 100644 index 0000000..a576329 --- /dev/null +++ b/plugins/code/ui/code_fold/__main__.py @@ -0,0 +1,3 @@ +""" + Pligin Package +""" diff --git a/plugins/code/ui/code_fold/fold_types.py b/plugins/code/ui/code_fold/fold_types.py new file mode 100644 index 0000000..946a128 --- /dev/null +++ b/plugins/code/ui/code_fold/fold_types.py @@ -0,0 +1,42 @@ +# Python imports + +# Lib imports + +# Application imports + + + +FOLD_NODES = { + "python": { + "function_definition", + "class_definition", + "if_statement", + "for_statement", + "while_statement", + "with_statement", + "try_statement", + }, + "python3": { + "function_definition", + "class_definition", + "if_statement", + "for_statement", + "while_statement", + "with_statement", + "try_statement", + }, + "java": { + "class_declaration", + "method_declaration", + "constructor_declaration", + "if_statement", + "for_statement", + "while_statement", + "switch_expression", + "block", + }, + "json": { + "object", + "array", + }, +} \ No newline at end of file diff --git a/plugins/code/ui/code_fold/folding_actions.py b/plugins/code/ui/code_fold/folding_actions.py new file mode 100644 index 0000000..02e4af8 --- /dev/null +++ b/plugins/code/ui/code_fold/folding_actions.py @@ -0,0 +1,22 @@ +# Python imports + +# Lib imports + +# Application imports + + + +def collapse_range(view, fold): + buffer = view.get_buffer() + start = buffer.get_iter_at_line(fold["start_line"] + 1) + end = buffer.get_iter_at_line(fold["end_line"] + 1) + + buffer.apply_tag_by_name("invisible", start, end) + + +def expand_range(view, fold): + buffer = view.get_buffer() + start = buffer.get_iter_at_line(fold["start_line"] + 1) + end = buffer.get_iter_at_line(fold["end_line"] + 1) + + buffer.remove_tag_by_name("invisible", start, end) diff --git a/plugins/code/ui/code_fold/folding_engine.py b/plugins/code/ui/code_fold/folding_engine.py new file mode 100644 index 0000000..c7ca86f --- /dev/null +++ b/plugins/code/ui/code_fold/folding_engine.py @@ -0,0 +1,31 @@ +# Python imports + +# Lib imports + +# Application imports +from .fold_types import FOLD_NODES + + + +def get_folding_ranges(lang_name: str, ast): + root = ast.root_node + fold_types = FOLD_NODES.get(lang_name, set()) + ranges = [] + + def visit(node): + if node.type in fold_types: + start_line = node.start_point[0] + end_line = node.end_point[0] + + if end_line > start_line: + ranges.append({ + "start_line": start_line, + "end_line": end_line, + "id": (start_line, end_line, node.type), + }) + + for child in node.children: + visit(child) + + visit(root) + return ranges diff --git a/plugins/code/ui/code_fold/gutter_renderer.py b/plugins/code/ui/code_fold/gutter_renderer.py new file mode 100644 index 0000000..dc77d3a --- /dev/null +++ b/plugins/code/ui/code_fold/gutter_renderer.py @@ -0,0 +1,93 @@ +# Python imports + +# Lib imports +import gi +gi.require_version("Gtk", "3.0") +gi.require_version("GtkSource", "4") + +from gi.repository import Gtk +from gi.repository import GtkSource + +# Application imports +from .folding_actions import collapse_range, expand_range + + + +def handle_collapse(view, fold): + collapse_range(view, fold) + + for inner in view.fold_starts: + if fold["start_line"] < inner["start_line"] <= fold["end_line"]: + view.fold_states[inner["id"]] = False + +def handle_expand(view, fold): + expand_range(view, fold) + +def handle_block_toggle(collapsed, view, fold): + if not collapsed: + handle_collapse(view, fold) + else: + handle_expand(view, fold) + + +def on_query_data(renderer, start_iter, end_iter, state, view): + line = start_iter.get_line() + + if not line in view.fold_start_set: + renderer.set_text("", -1) + return + + fold = next((f for f in view.fold_starts if f["start_line"] == line), None) + collapsed = fold and view.fold_states.get(fold["id"], False) + + renderer.set_text( "▶" if collapsed else "▼", -1 ) + +def on_click(view, event, renderer): + if not event.button == 1: return False + window = view.get_window(Gtk.TextWindowType.LEFT) + if not event.window == window: return False + + x, y = view.window_to_buffer_coords( + Gtk.TextWindowType.LEFT, int(event.x), int(event.y) + ) + + _, iter_ = view.get_iter_at_location(x, y) + line = iter_.get_line() + + if line not in view.fold_start_set: return False + + for fold in view.fold_starts: + if not fold["start_line"] == line: continue + + collapsed = view.fold_states.get(fold["id"], False) + view.fold_states[fold["id"]] = not collapsed + + handle_block_toggle(collapsed, view, fold) + + renderer.queue_draw() + return True + + return False + + +def setup_gutter(view): + gutter = view.get_gutter(Gtk.TextWindowType.LEFT) + buffer = view.get_buffer() + + if not buffer.get_tag_table().lookup("invisible"): + tag = buffer.create_tag("invisible") + tag.set_property("invisible", True) + + view.fold_starts = [] + view.fold_start_set = set() + view.fold_states = {} + + renderer = GtkSource.GutterRendererText() + renderer.set_size(12) + renderer.set_padding(2, -1) + + renderer.query_data_id = renderer.connect("query-data", on_query_data, view) + view.collapse_click_id = view.connect("button-press-event", on_click, renderer) + + gutter.insert(renderer, 0) + view.fold_renderer = renderer diff --git a/plugins/code/ui/code_fold/manifest.json b/plugins/code/ui/code_fold/manifest.json new file mode 100644 index 0000000..b9ccebd --- /dev/null +++ b/plugins/code/ui/code_fold/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Code Fold", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "requests": {} +} diff --git a/plugins/code/ui/code_fold/plugin.py b/plugins/code/ui/code_fold/plugin.py new file mode 100644 index 0000000..b1bb2c9 --- /dev/null +++ b/plugins/code/ui/code_fold/plugin.py @@ -0,0 +1,98 @@ +# Python imports + +# Lib imports +import gi + +gi.require_version("Gtk", "3.0") + +from gi.repository import GLib +from gi.repository import Gtk + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types +from plugins.plugin_types import PluginCode + +from .fold_types import FOLD_NODES +from .folding_engine import get_folding_ranges +from .gutter_renderer import setup_gutter + + + +class Plugin(PluginCode): + def _controller_message(self, event): + if isinstance(event, Code_Event_Types.FocusedViewEvent): + self.view = event.view + + event = Event_Factory.create_event( + "get_file", buffer=self.view.get_buffer() + ) + self.emit_to("files", event) + + file = event.response + + if not file: return + if file.ftype not in FOLD_NODES: return + if not hasattr(file, "ast"): return + + self.update_gutter(file, self.view) + elif isinstance(event, Code_Event_Types.TextChangedEvent): + if event.file.ftype not in FOLD_NODES: return + if not hasattr(event.file, "ast"): return + + self.schedule_update(event.file, self.view) + + def load(self): + event = Event_Factory.create_event("get_source_views") + self.emit_to("source_views", event) + + for view in event.response: + setup_gutter(view) + + def unload(self): + event = Event_Factory.create_event("get_source_views") + self.emit_to("source_views", event) + + for view in event.response: + view.fold_renderer.disconnect(view.fold_renderer.query_data_id) + view.disconnect(view.collapse_click_id) + + gutter = view.get_gutter(Gtk.TextWindowType.LEFT) + gutter.remove(view.fold_renderer) + + def run(self): + ... + + def schedule_update(self, file, view, delay=250): + if hasattr(view, "fold_update_source") and view.fold_update_source: + GLib.source_remove(view.fold_update_source) + + def callback(): + self.update_gutter(file, view) + + if hasattr(view, "fold_renderer"): + view.fold_renderer.queue_draw() + + view.fold_update_source = None + return False + + view.fold_update_source = GLib.timeout_add(delay, callback) + + def update_gutter(self, file, view): + old_states = getattr(view, "fold_states", {}) + + view.fold_starts = get_folding_ranges(file.ftype, file.ast) + view.fold_start_set = { + fold["start_line"] for fold in view.fold_starts + } + + buffer = view.get_buffer() + if not buffer.get_tag_table().lookup("invisible"): + tag = buffer.create_tag("invisible") + tag.set_property("invisible", True) + + new_states = {} + for fold in view.fold_starts: + if not fold["id"] in old_states: continue + new_states[fold["id"]] = old_states[fold["id"]] + + view.fold_states = new_states diff --git a/src/core/widgets/code/controllers/views/source_views_controller.py b/src/core/widgets/code/controllers/views/source_views_controller.py index cb66763..6503cdf 100644 --- a/src/core/widgets/code/controllers/views/source_views_controller.py +++ b/src/core/widgets/code/controllers/views/source_views_controller.py @@ -111,6 +111,12 @@ class SourceViewsController(ControllerBase, list): self.signal_mapper.connect_signals(source_view) self.append(source_view) + + event = Event_Factory.create_event( + "created_source_view", view = source_view + ) + self.emit(event) + return source_view def first_map_load(self): diff --git a/src/libs/dto/code/events/__init__.py b/src/libs/dto/code/events/__init__.py index cfe8223..0079fd5 100644 --- a/src/libs/dto/code/events/__init__.py +++ b/src/libs/dto/code/events/__init__.py @@ -6,6 +6,7 @@ from .code_event import CodeEvent from .toggle_plugins_ui_event import TogglePluginsUiEvent from .create_source_view_event import CreateSourceViewEvent +from .created_source_view_event import CreatedSourceViewEvent from .register_completer_event import RegisterCompleterEvent from .unregister_completer_event import UnregisterCompleterEvent from .register_provider_event import RegisterProviderEvent diff --git a/src/libs/dto/code/events/created_source_view_event.py b/src/libs/dto/code/events/created_source_view_event.py new file mode 100644 index 0000000..19ed377 --- /dev/null +++ b/src/libs/dto/code/events/created_source_view_event.py @@ -0,0 +1,14 @@ +# Python imports +from dataclasses import dataclass + +# Lib imports + +# Application imports +from .code_event import CodeEvent +from libs.dto.states.source_view_states import SourceViewStates + + + +@dataclass +class CreatedSourceViewEvent(CodeEvent): + ...