Files
gdb-gui/gui/source.py
Tom Tromey 48b2b25526 add flake8
2025-12-06 13:18:16 -06:00

411 lines
13 KiB
Python

# Copyright (C) 2012, 2013, 2015, 2023, 2024, 2025 Tom Tromey <tom@tromey.com>
# 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 <http://www.gnu.org/licenses/>.
# 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)