# Copyright (C) 2012, 2013, 2015, 2023, 2024, 2025 Tom Tromey # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Source view. import os.path import gdb from gi.repository import Gdk, GdkPixbuf, GObject, GtkSource import gui import gui.bpcache import gui.events import gui.gdbutil import gui.params import gui.startup import gui.toplevel import gui.updatewindow from gui.framecache import FrameCommandInvoker from gui.invoker import Invoker from gui.startup import in_gdb_thread, in_gtk_thread class BufferManager: def __init__(self): self.buffers = {} self.lang_manager = None gui.params.source_theme.set_buffer_manager(self) gui.events.location_changed.connect(self._location_changed) # FIXME - emit a warning if this isn't available. if hasattr(gdb.events, "clear_objfiles"): gdb.events.clear_objfiles.connect(self._clear_objfiles) self.empty_buffer = None def release_buffer(self, buff): # FIXME: we should be smart about buffer caching. # Note that BUFF can be the empty buffer. pass @in_gtk_thread def get_empty_buffer(self): if self.empty_buffer is None: self.empty_buffer = GtkSource.Buffer() self.empty_buffer.set_style_scheme(gui.params.source_theme.get_scheme()) return self.empty_buffer @in_gtk_thread def _set_marks(self, buffer, line_set): iter = buffer.get_iter_at_line(0) while True: line = iter.get_line() + 1 if line in line_set: buffer.create_source_mark(None, "executable", iter) if not iter.forward_line(): break @in_gdb_thread def _get_lines_update(self, buffer, symtab): if hasattr(symtab, "linetable"): line_set = set(symtab.linetable().source_lines()) gui.startup.send_to_gtk(lambda: self._set_marks(buffer, line_set)) @in_gtk_thread def get_buffer(self, symtab, filename): if filename in self.buffers: return self.buffers[filename] if not self.lang_manager: self.lang_manager = GtkSource.LanguageManager.get_default() buff = GtkSource.Buffer() if filename: buff.set_language(self.lang_manager.guess_language(filename)) buff.set_style_scheme(gui.params.source_theme.get_scheme()) buff.begin_not_undoable_action() try: contents = open(filename).read() except Exception: return None buff.set_text(contents) buff.end_not_undoable_action() buff.set_modified(False) buff.filename = filename if symtab is not None: gdb.post_event(lambda: self._get_lines_update(buff, symtab)) self.buffers[filename] = buff return buff @in_gtk_thread def _do_change_theme(self): new_scheme = gui.params.source_theme.get_scheme() for filename in self.buffers: self.buffers[filename].set_style_scheme(new_scheme) if self.empty_buffer is not None: self.empty_buffer.set_style_scheme(new_scheme) @in_gdb_thread def change_theme(self): gui.startup.send_to_gtk(self._do_change_theme) @in_gtk_thread def update_breakpoint_location(self, sal, is_set): if is_set: category = "breakpoint" else: category = "executable" [fullname, line] = sal if fullname in self.buffers: buffer = self.buffers[fullname] iter = buffer.get_iter_at_line(line - 1) buffer.remove_source_marks(iter, iter) buffer.create_source_mark(None, category, iter) @in_gdb_thread def _location_changed(self, loc, is_set): gui.startup.send_to_gtk(lambda: self.update_breakpoint_location(loc, is_set)) @in_gtk_thread def _gtk_clear_objfiles(self): empty_buffer = self.get_empty_buffer() for window in gui.toplevel.state.windows(): window.clear_source(empty_buffer) self.buffers = {} @in_gdb_thread def _clear_objfiles(self, ignore): gui.startup.send_to_gtk(self._gtk_clear_objfiles) @in_gtk_thread def clear_last_pointer(self): for key in self.buffers: buff = self.buffers[key] # This could probably be more efficient. buff.remove_source_marks( buff.get_start_iter(), buff.get_end_iter(), "pointer" ) buffer_manager = BufferManager() # Return (FRAME, SYMTAB, FILE, LINE) for the selected frame, or, if # there is no frame, for "main". @in_gdb_thread def get_current_location(): try: frame = gdb.selected_frame() sal = frame.find_sal() symtab = sal.symtab if symtab is not None: filename = symtab.fullname() else: filename = None lineno = sal.line except gdb.error: # FIXME: should use the static location as set by list etc. # No frame - try 'main'. try: frame = None sym = gdb.lookup_global_symbol("main") lineno = sym.line symtab = sym.symtab filename = symtab.fullname() except gdb.error: # Perhaps no symbol file. return (None, None, None, None) except AttributeError: return (None, None, None, None) return (frame, symtab, filename, lineno) class LRUHandler: def __init__(self): self.windows = [] self.work_location = None # What a lame name. @in_gdb_thread def show_source_gdb(self, frame, symtab, srcfile, srcline): if len(self.windows) == 0: self.work_location = (frame, symtab, srcfile, srcline) SourceWindow() gui.startup.send_to_gtk( lambda: self.show_source(frame, symtab, srcfile, srcline) ) @in_gdb_thread def new_source_window(self): self.work_location = get_current_location() SourceWindow() @in_gdb_thread def on_event(self, *args): (frame, symtab, filename, lineno) = get_current_location() if filename is not None: gui.startup.send_to_gtk( lambda: self.show_source(frame, symtab, filename, lineno) ) @in_gdb_thread def _connect_events(self): gdb.events.stop.connect(self.on_event) gui.events.frame_changed.connect(self.on_event) if hasattr(gdb.events, "new_objfile"): gdb.events.new_objfile.connect(self._new_objfile) @in_gdb_thread def _disconnect_events(self): gdb.events.stop.disconnect(self.on_event) gui.events.frame_changed.disconnect(self.on_event) @in_gtk_thread def pick_window(self, frame): # If a window is showing FRAME, use it. # Otherwise, if a window has no frame, use that. # Otherwise, use the first window. no_frame = None for w in self.windows: # Perhaps this is technically not ok. # We should document thread-safety a bit better. # Or just fix it up. if frame == w.frame: return w if w.frame is None and no_frame is None: no_frame = w if no_frame is not None: return no_frame return self.windows[0] @in_gtk_thread def show_source(self, frame, symtab, srcfile, srcline): w = self.pick_window(frame) # LRU policy. self.windows.remove(w) self.windows.append(w) w.frame = frame w.show_source(symtab, srcfile, srcline) @in_gtk_thread def remove(self, window): self.windows.remove(window) if len(self.windows) == 0: gdb.post_event(self._disconnect_events) @in_gtk_thread def add(self, window): self.windows.insert(0, window) if len(self.windows) == 1: gdb.post_event(self._connect_events) # Show something. if self.work_location is not None: (frame, symtab, filename, lineno) = self.work_location self.work_location = None gui.startup.send_to_gtk( lambda: self.show_source(frame, symtab, filename, lineno) ) @in_gdb_thread def _new_objfile(self, event): if len(gdb.objfiles()) == 1: self.on_event() lru_handler = LRUHandler() BUTTON_NAMES = ["step", "next", "continue", "finish", "stop", "up", "down"] class SourceWindow(gui.updatewindow.UpdateWindow): def _get_pixmap(self, filename): path = os.path.join(gui.self_dir, filename) return GdkPixbuf.Pixbuf.new_from_file(path) def __init__(self): super(SourceWindow, self).__init__("source") gdb.events.cont.connect(self._on_cont_event) # Update the buttons. self.on_event() @in_gtk_thread def gtk_initialize(self): self.frame = None self.do_step = Invoker("step") self.do_next = Invoker("next") self.do_continue = Invoker("continue") self.do_finish = Invoker("finish") self.do_stop = Invoker("interrupt") self.do_up = FrameCommandInvoker("up") self.do_down = FrameCommandInvoker("down") builder = gui.startup.create_builder("sourcewindow.xml") builder.connect_signals(self) self.window = builder.get_object("sourcewindow") self.view = builder.get_object("view") # Maybe there is a cleaner way? self.buttons = {} for name in BUTTON_NAMES: self.buttons[name] = builder.get_object(name) self.view.modify_font(gui.params.font_manager.get_font()) self.view.set_show_line_numbers(gui.params.line_numbers.value) self.view.set_tab_width(gui.params.tab_width.value) attrs = GtkSource.MarkAttributes() attrs.set_pixbuf(self._get_pixmap("icons/ok.png")) self.view.set_mark_attributes("executable", attrs, 0) attrs = GtkSource.MarkAttributes() attrs.set_pixbuf(self._get_pixmap("icons/breakpoint-marker.png")) self.view.set_mark_attributes("breakpoint", attrs, 1) attrs = GtkSource.MarkAttributes() attrs.set_pixbuf(self._get_pixmap("icons/line-pointer.png")) self.view.set_mark_attributes("pointer", attrs, 2) self.view.set_buffer(buffer_manager.get_empty_buffer()) lru_handler.add(self) @in_gtk_thread def _update_buttons(self, running): for button in BUTTON_NAMES: if button == "stop": self.buttons[button].set_sensitive(running) else: self.buttons[button].set_sensitive(not running) @in_gdb_thread def on_event(self): running = gui.gdbutil.is_running() gui.startup.send_to_gtk(lambda: self._update_buttons(running)) @in_gdb_thread def _on_cont_event(self, event): self.on_event() @in_gdb_thread def _disconnect_cont_event(self): gdb.events.cont.disconnect(self._on_cont_event) def deleted(self, *args): lru_handler.remove(self) gdb.post_event(self._disconnect_cont_event) super(SourceWindow, self).deleted() def line_mark_activated(self, view, textiter, event): if event.type != Gdk.EventType.BUTTON_PRESS: return if event.button.get_button()[1] != 1: return filename = self.view.get_buffer().filename line = textiter.get_line() + 1 if gui.bpcache.any_breakpoint_at(filename, line): fun = Invoker("clear %s:%d" % (filename, line)) else: fun = Invoker("break %s:%d" % (filename, line)) fun() def _do_scroll(self, buff, srcline): iter = buff.get_iter_at_line(srcline) buff.create_source_mark(None, "pointer", iter) buff.place_cursor(iter) self.view.scroll_mark_onscreen(buff.get_insert()) return False def show_source(self, symtab, srcfile, srcline): buff = buffer_manager.get_buffer(symtab, srcfile) if buff is not None: old_buffer = self.view.get_buffer() self.view.set_buffer(buff) self.fullname = srcfile self.basename = os.path.basename(srcfile) self.update_title() buffer_manager.release_buffer(old_buffer) buffer_manager.clear_last_pointer() GObject.idle_add(self._do_scroll, buff, srcline - 1) # self.view.scroll_to_iter(buff.get_iter_at_line(srcline), 0.0) @in_gtk_thread def set_font(self, pango_font): self.view.modify_font(pango_font) @in_gtk_thread def set_line_numbers(self, want_lines): self.view.set_show_line_numbers(want_lines) @in_gtk_thread def set_tab_width(self, width): self.view.set_tab_width(width) @in_gtk_thread def clear_source(self, buffer): old_buffer = self.view.get_buffer() self.view.set_buffer(buffer) buffer_manager.release_buffer(old_buffer)