From ba429d344132c088177e853cce8ff7181570b221 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 19:42:51 +0200 Subject: Adding upstream version 44.2. Signed-off-by: Daniel Baumann --- plugins/externaltools/tools/__init__.py | 26 + plugins/externaltools/tools/appactivatable.py | 178 +++++ plugins/externaltools/tools/capture.py | 268 +++++++ plugins/externaltools/tools/filelookup.py | 165 +++++ plugins/externaltools/tools/functions.py | 365 ++++++++++ plugins/externaltools/tools/library.py | 520 ++++++++++++++ plugins/externaltools/tools/linkparsing.py | 252 +++++++ plugins/externaltools/tools/manager.py | 878 +++++++++++++++++++++++ plugins/externaltools/tools/meson.build | 36 + plugins/externaltools/tools/outputpanel.py | 247 +++++++ plugins/externaltools/tools/outputpanel.ui | 49 ++ plugins/externaltools/tools/tools.ui | 548 ++++++++++++++ plugins/externaltools/tools/windowactivatable.py | 141 ++++ 13 files changed, 3673 insertions(+) create mode 100644 plugins/externaltools/tools/__init__.py create mode 100644 plugins/externaltools/tools/appactivatable.py create mode 100644 plugins/externaltools/tools/capture.py create mode 100644 plugins/externaltools/tools/filelookup.py create mode 100644 plugins/externaltools/tools/functions.py create mode 100644 plugins/externaltools/tools/library.py create mode 100644 plugins/externaltools/tools/linkparsing.py create mode 100644 plugins/externaltools/tools/manager.py create mode 100644 plugins/externaltools/tools/meson.build create mode 100644 plugins/externaltools/tools/outputpanel.py create mode 100644 plugins/externaltools/tools/outputpanel.ui create mode 100644 plugins/externaltools/tools/tools.ui create mode 100644 plugins/externaltools/tools/windowactivatable.py (limited to 'plugins/externaltools/tools') diff --git a/plugins/externaltools/tools/__init__.py b/plugins/externaltools/tools/__init__.py new file mode 100644 index 0000000..0cc0b4f --- /dev/null +++ b/plugins/externaltools/tools/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: UTF-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2010 Ignacio Casal Quinteiro +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gi +gi.require_version('Gedit', '3.0') +gi.require_version('Gtk', '3.0') + +from .appactivatable import AppActivatable +from .windowactivatable import WindowActivatable + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/appactivatable.py b/plugins/externaltools/tools/appactivatable.py new file mode 100644 index 0000000..87e1226 --- /dev/null +++ b/plugins/externaltools/tools/appactivatable.py @@ -0,0 +1,178 @@ +# -*- coding: UTF-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gi.repository import GLib, Gio, GObject, Gtk, Gdk, Gedit +from .library import ToolLibrary +from .manager import Manager +import os + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +class ToolMenu(object): + def __init__(self, library, menu): + super(ToolMenu, self).__init__() + self._library = library + self._menu = menu + self._action_tools = {} + + self.update() + + def deactivate(self): + self.remove() + + def remove(self): + self._menu.remove_all() + + for name, tool in self._action_tools.items(): + if tool.shortcut: + app = Gio.Application.get_default() + app.remove_accelerator(tool.shortcut) + + def _insert_directory(self, directory, menu): + for d in sorted(directory.subdirs, key=lambda x: x.name.lower()): + submenu = Gio.Menu() + menu.append_submenu(d.name.replace('_', '__'), submenu) + section = Gio.Menu() + submenu.append_section(None, section) + + self._insert_directory(d, section) + + for tool in sorted(directory.tools, key=lambda x: x.name.lower()): + # FIXME: find a better way to share the action name + action_name = 'external-tool-%X-%X' % (id(tool), id(tool.name)) + item = Gio.MenuItem.new(tool.name.replace('_', '__'), "win.%s" % action_name) + item.set_attribute_value("hidden-when", GLib.Variant.new_string("action-disabled")) + menu.append_item(item) + + if tool.shortcut: + app = Gio.Application.get_default() + app.add_accelerator(tool.shortcut, "win.%s" % action_name, None) + + def update(self): + self.remove() + self._insert_directory(self._library.tree, self._menu) + + +# FIXME: restore the launch of the manager on configure using PeasGtk.Configurable +class AppActivatable(GObject.Object, Gedit.AppActivatable): + __gtype_name__ = "ExternalToolsAppActivatable" + + app = GObject.Property(type=Gedit.App) + + def __init__(self): + GObject.Object.__init__(self) + self.menu = None + self._manager = None + self._manager_default_size = None + + def do_activate(self): + self._library = ToolLibrary() + self._library.set_locations(os.path.join(self.plugin_info.get_data_dir(), 'tools')) + + action = Gio.SimpleAction(name="manage-tools") + action.connect("activate", lambda action, parameter: self._open_dialog()) + self.app.add_action(action) + + self.css = Gtk.CssProvider() + self.css.load_from_data(""" +.gedit-tool-manager-paned { + border-style: solid; + border-color: @borders; +} + +.gedit-tool-manager-paned:dir(ltr) { + border-width: 0 1px 0 0; +} + +.gedit-tool-manager-paned:dir(rtl) { + border-width: 0 0 0 1px; +} + +.gedit-tool-manager-view { + border-width: 0 0 1px 0; +} + +.gedit-tool-manager-treeview { + border-top-width: 0; +} + +.gedit-tool-manager-treeview:dir(ltr) { + border-left-width: 0; +} + +.gedit-tool-manager-treeview:dir(rtl) { + border-right-width: 0; +} +""".encode('utf-8')) + + Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), + self.css, 600) + + self.menu_ext = self.extend_menu("preferences-section") + item = Gio.MenuItem.new(_("Manage _External Tools…"), "app.manage-tools") + self.menu_ext.append_menu_item(item) + + self.submenu_ext = self.extend_menu("tools-section-1") + external_tools_submenu = Gio.Menu() + item = Gio.MenuItem.new_submenu(_("External _Tools"), external_tools_submenu) + self.submenu_ext.append_menu_item(item) + external_tools_submenu_section = Gio.Menu() + external_tools_submenu.append_section(None, external_tools_submenu_section) + + self.menu = ToolMenu(self._library, external_tools_submenu_section) + + def do_deactivate(self): + self.menu.deactivate() + self.menu_ext = None + self.submenu_ext = None + + self.app.remove_action("manage-tools") + + Gtk.StyleContext.remove_provider_for_screen(Gdk.Screen.get_default(), + self.css) + + def _open_dialog(self): + if not self._manager: + self._manager = Manager(self.plugin_info.get_data_dir()) + + if self._manager_default_size: + self._manager.dialog.set_default_size(*self._manager_default_size) + + self._manager.dialog.connect('destroy', self._on_manager_destroy) + self._manager.connect('tools-updated', self._on_manager_tools_updated) + + self._manager.run(self.app.get_active_window()) + + return self._manager.dialog + + def _on_manager_destroy(self, dialog): + self._manager_default_size = self._manager.get_final_size() + self._manager = None + + def _on_manager_tools_updated(self, manager): + for window in self.app.get_main_windows(): + window.external_tools_window_activatable.update_actions() + self.menu.update() + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/capture.py b/plugins/externaltools/tools/capture.py new file mode 100644 index 0000000..d7560c5 --- /dev/null +++ b/plugins/externaltools/tools/capture.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +__all__ = ('Capture', ) + +import os +import sys +import signal +import locale +import subprocess +import fcntl +from gi.repository import GLib, GObject + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +class Capture(GObject.Object): + CAPTURE_STDOUT = 0x01 + CAPTURE_STDERR = 0x02 + CAPTURE_BOTH = 0x03 + CAPTURE_NEEDS_SHELL = 0x04 + + WRITE_BUFFER_SIZE = 0x4000 + + __gsignals__ = { + 'stdout-line': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), + 'stderr-line': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), + 'begin-execute': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, tuple()), + 'end-execute': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_INT,)) + } + + def __init__(self, command, cwd=None, env={}): + GObject.GObject.__init__(self) + self.pipe = None + self.env = env + self.cwd = cwd + self.flags = self.CAPTURE_BOTH | self.CAPTURE_NEEDS_SHELL + self.command = command + self.input_text = None + + def set_env(self, **values): + self.env.update(**values) + + def set_command(self, command): + self.command = command + + def set_flags(self, flags): + self.flags = flags + + def set_input(self, text): + self.input_text = text.encode("UTF-8") if text else None + + def set_cwd(self, cwd): + self.cwd = cwd + + def execute(self): + if self.command is None: + return + + # Initialize pipe + popen_args = { + 'cwd': self.cwd, + 'shell': self.flags & self.CAPTURE_NEEDS_SHELL, + 'env': self.env + } + + if self.input_text is not None: + popen_args['stdin'] = subprocess.PIPE + if self.flags & self.CAPTURE_STDOUT: + popen_args['stdout'] = subprocess.PIPE + if self.flags & self.CAPTURE_STDERR: + popen_args['stderr'] = subprocess.PIPE + + self.tried_killing = False + self.in_channel = None + self.out_channel = None + self.err_channel = None + self.in_channel_id = 0 + self.out_channel_id = 0 + self.err_channel_id = 0 + + try: + self.pipe = subprocess.Popen(self.command, **popen_args) + except OSError as e: + self.pipe = None + self.emit('stderr-line', _('Could not execute command: %s') % (e, )) + return + + self.emit('begin-execute') + + if self.input_text is not None: + self.in_channel, self.in_channel_id = self.add_in_watch(self.pipe.stdin.fileno(), + self.on_in_writable) + + if self.flags & self.CAPTURE_STDOUT: + self.out_channel, self.out_channel_id = self.add_out_watch(self.pipe.stdout.fileno(), + self.on_output) + + if self.flags & self.CAPTURE_STDERR: + self.err_channel, self.err_channel_id = self.add_out_watch(self.pipe.stderr.fileno(), + self.on_err_output) + + # Wait for the process to complete + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, + self.pipe.pid, + self.on_child_end) + + def add_in_watch(self, fd, io_func): + channel = GLib.IOChannel.unix_new(fd) + channel.set_flags(channel.get_flags() | GLib.IOFlags.NONBLOCK) + channel.set_encoding(None) + channel_id = GLib.io_add_watch(channel, + GLib.PRIORITY_DEFAULT, + GLib.IOCondition.OUT | GLib.IOCondition.HUP | GLib.IOCondition.ERR, + io_func) + return (channel, channel_id) + + def add_out_watch(self, fd, io_func): + channel = GLib.IOChannel.unix_new(fd) + channel.set_flags(channel.get_flags() | GLib.IOFlags.NONBLOCK) + channel_id = GLib.io_add_watch(channel, + GLib.PRIORITY_DEFAULT, + GLib.IOCondition.IN | GLib.IOCondition.HUP | GLib.IOCondition.ERR, + io_func) + return (channel, channel_id) + + def write_chunk(self, dest, condition): + if condition & (GObject.IO_OUT): + status = GLib.IOStatus.NORMAL + l = len(self.input_text) + while status == GLib.IOStatus.NORMAL: + if l == 0: + return False + m = min(l, self.WRITE_BUFFER_SIZE) + try: + (status, length) = dest.write_chars(self.input_text, m) + self.input_text = self.input_text[length:] + l -= length + except Exception as e: + return False + if status != GLib.IOStatus.AGAIN: + return False + + if condition & ~(GObject.IO_OUT): + return False + + return True + + def on_in_writable(self, dest, condition): + ret = self.write_chunk(dest, condition) + if ret is False: + self.input_text = None + try: + self.in_channel.shutdown(True) + except: + pass + self.in_channel = None + self.in_channel_id = 0 + self.cleanup_pipe() + + return ret + + def handle_source(self, source, condition, signalname): + if condition & (GObject.IO_IN | GObject.IO_PRI): + status = GLib.IOStatus.NORMAL + while status == GLib.IOStatus.NORMAL: + try: + (status, buf, length, terminator_pos) = source.read_line() + except Exception as e: + return False + if buf: + self.emit(signalname, buf) + if status != GLib.IOStatus.AGAIN: + return False + + if condition & ~(GObject.IO_IN | GObject.IO_PRI): + return False + + return True + + def on_output(self, source, condition): + ret = self.handle_source(source, condition, 'stdout-line') + if ret is False and self.out_channel: + try: + self.out_channel.shutdown(True) + except: + pass + self.out_channel = None + self.out_channel_id = 0 + self.cleanup_pipe() + + return ret + + def on_err_output(self, source, condition): + ret = self.handle_source(source, condition, 'stderr-line') + if ret is False and self.err_channel: + try: + self.err_channel.shutdown(True) + except: + pass + self.err_channel = None + self.err_channel_id = 0 + self.cleanup_pipe() + + return ret + + def cleanup_pipe(self): + if self.in_channel is None and self.out_channel is None and self.err_channel is None: + self.pipe = None + + def stop(self, error_code=-1): + if self.in_channel_id: + GLib.source_remove(self.in_channel_id) + self.in_channel.shutdown(True) + self.in_channel = None + self.in_channel_id = 0 + + if self.out_channel_id: + GLib.source_remove(self.out_channel_id) + self.out_channel.shutdown(True) + self.out_channel = None + self.out_channel_id = 0 + + if self.err_channel_id: + GLib.source_remove(self.err_channel_id) + self.err_channel.shutdown(True) + self.err_channel = None + self.err_channel = 0 + + if self.pipe is not None: + if not self.tried_killing: + os.kill(self.pipe.pid, signal.SIGTERM) + self.tried_killing = True + else: + os.kill(self.pipe.pid, signal.SIGKILL) + + self.pipe = None + + def emit_end_execute(self, error_code): + self.emit('end-execute', error_code) + return False + + def on_child_end(self, pid, error_code): + # In an idle, so it is emitted after all the std*-line signals + # have been intercepted + GLib.idle_add(self.emit_end_execute, error_code) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/filelookup.py b/plugins/externaltools/tools/filelookup.py new file mode 100644 index 0000000..f256eea --- /dev/null +++ b/plugins/externaltools/tools/filelookup.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2010 Per Arneng +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +from gi.repository import Gio, Gedit +from .functions import * + + +class FileLookup: + """ + This class is responsible for looking up files given a part or the whole + path of a real file. The lookup is delegated to providers wich use + different methods of trying to find the real file. + """ + + def __init__(self, window): + self.providers = [] + self.providers.append(AbsoluteFileLookupProvider()) + self.providers.append(BrowserRootFileLookupProvider(window)) + self.providers.append(CwdFileLookupProvider()) + self.providers.append(OpenDocumentRelPathFileLookupProvider()) + self.providers.append(OpenDocumentFileLookupProvider()) + + def lookup(self, path): + """ + Tries to find a file specified by the path parameter. It delegates to + different lookup providers and the first match is returned. If no file + was found then None is returned. + + path -- the path to find + """ + found_file = None + for provider in self.providers: + found_file = provider.lookup(path) + if found_file is not None: + break + + return found_file + + +class FileLookupProvider: + """ + The base class of all file lookup providers. + """ + + def lookup(self, path): + """ + This method must be implemented by subclasses. Implementors will be + given a path and will try to find a matching file. If no file is found + then None is returned. + """ + raise NotImplementedError("need to implement a lookup method") + + +class AbsoluteFileLookupProvider(FileLookupProvider): + """ + This file tries to see if the path given is an absolute path and that the + path references a file. + """ + + def lookup(self, path): + if os.path.isabs(path) and os.path.isfile(path): + return Gio.file_new_for_path(path) + else: + return None + + +class BrowserRootFileLookupProvider(FileLookupProvider): + """ + This lookup provider tries to find a file specified by the path relative to + the file browser root. + """ + def __init__(self, window): + self.window = window + + def lookup(self, path): + root = file_browser_root(self.window) + if root: + real_path = os.path.join(root, path) + if os.path.isfile(real_path): + return Gio.file_new_for_path(real_path) + + return None + + +class CwdFileLookupProvider(FileLookupProvider): + """ + This lookup provider tries to find a file specified by the path relative to + the current working directory. + """ + + def lookup(self, path): + try: + cwd = os.getcwd() + except OSError: + cwd = os.getenv('HOME') + + real_path = os.path.join(cwd, path) + + if os.path.isfile(real_path): + return Gio.file_new_for_path(real_path) + else: + return None + + +class OpenDocumentRelPathFileLookupProvider(FileLookupProvider): + """ + Tries to see if the path is relative to any directories where the + currently open documents reside in. Example: If you have a document opened + '/tmp/Makefile' and a lookup is made for 'src/test2.c' then this class + will try to find '/tmp/src/test2.c'. + """ + + def lookup(self, path): + if path.startswith('/'): + return None + + for doc in Gio.Application.get_default().get_documents(): + if doc.get_file().is_local(): + location = doc.get_file().get_location() + if location: + rel_path = location.get_parent().get_path() + joined_path = os.path.join(rel_path, path) + if os.path.isfile(joined_path): + return Gio.file_new_for_path(joined_path) + + return None + + +class OpenDocumentFileLookupProvider(FileLookupProvider): + """ + Makes a guess that the if the path that was looked for matches the end + of the path of a currently open document then that document is the one + that is looked for. Example: If a document is opened called '/tmp/t.c' + and a lookup is made for 't.c' or 'tmp/t.c' then both will match since + the open document ends with the path that is searched for. + """ + + def lookup(self, path): + if path.startswith('/'): + return None + + for doc in Gio.Application.get_default().get_documents(): + if doc.get_file().is_local(): + location = doc.get_file().get_location() + if location and location.get_uri().endswith(path): + return location + return None + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/functions.py b/plugins/externaltools/tools/functions.py new file mode 100644 index 0000000..bc755be --- /dev/null +++ b/plugins/externaltools/tools/functions.py @@ -0,0 +1,365 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +from gi.repository import Gio, Gtk, Gdk, GtkSource, Gedit +from .capture import * + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +def default(val, d): + if val is not None: + return val + else: + return d + + +def current_word(document): + piter = document.get_iter_at_mark(document.get_insert()) + start = piter.copy() + + if not piter.starts_word() and (piter.inside_word() or piter.ends_word()): + start.backward_word_start() + + if not piter.ends_word() and piter.inside_word(): + piter.forward_word_end() + + return (start, piter) + + +def file_browser_root(window): + bus = window.get_message_bus() + + if bus.is_registered('/plugins/filebrowser', 'get_root'): + msg = bus.send_sync('/plugins/filebrowser', 'get_root') + + if msg: + browser_root = msg.props.location + + if browser_root and browser_root.is_native(): + return browser_root.get_path() + + return None + + +# ==== Capture related functions ==== +def run_external_tool(window, panel, node): + # Configure capture environment + try: + cwd = os.getcwd() + except OSError: + cwd = os.getenv('HOME') + + capture = Capture(node.command, cwd) + capture.env = os.environ.copy() + capture.set_env(GEDIT_CWD=cwd) + + view = window.get_active_view() + document = None + + if view is not None: + # Environment vars relative to current document + document = view.get_buffer() + location = document.get_file().get_location() + + # Current line number + piter = document.get_iter_at_mark(document.get_insert()) + capture.set_env(GEDIT_CURRENT_LINE_NUMBER=str(piter.get_line() + 1)) + + # Current line text + piter.set_line_offset(0) + end = piter.copy() + + if not end.ends_line(): + end.forward_to_line_end() + + capture.set_env(GEDIT_CURRENT_LINE=piter.get_text(end)) + + if document.get_language() is not None: + capture.set_env(GEDIT_CURRENT_DOCUMENT_LANGUAGE=document.get_language().get_id()) + + # Selected text (only if input is not selection) + if node.input != 'selection' and node.input != 'selection-document': + bounds = document.get_selection_bounds() + + if bounds: + capture.set_env(GEDIT_SELECTED_TEXT=bounds[0].get_text(bounds[1])) + + bounds = current_word(document) + capture.set_env(GEDIT_CURRENT_WORD=bounds[0].get_text(bounds[1])) + + capture.set_env(GEDIT_CURRENT_DOCUMENT_TYPE=document.get_mime_type()) + + if location is not None: + scheme = location.get_uri_scheme() + name = location.get_basename() + capture.set_env(GEDIT_CURRENT_DOCUMENT_URI=location.get_uri(), + GEDIT_CURRENT_DOCUMENT_NAME=name, + GEDIT_CURRENT_DOCUMENT_SCHEME=scheme) + if location.has_uri_scheme('file'): + path = location.get_path() + cwd = os.path.dirname(path) + capture.set_cwd(cwd) + capture.set_env(GEDIT_CURRENT_DOCUMENT_PATH=path, + GEDIT_CURRENT_DOCUMENT_DIR=cwd) + + documents_location = [doc.get_file().get_location() + for doc in window.get_documents() + if doc.get_file().get_location() is not None] + documents_uri = [location.get_uri() + for location in documents_location + if location.get_uri() is not None] + documents_path = [location.get_path() + for location in documents_location + if location.has_uri_scheme('file')] + capture.set_env(GEDIT_DOCUMENTS_URI=' '.join(documents_uri), + GEDIT_DOCUMENTS_PATH=' '.join(documents_path)) + + # set file browser root env var if possible + browser_root = file_browser_root(window) + if browser_root: + capture.set_env(GEDIT_FILE_BROWSER_ROOT=browser_root) + + flags = capture.CAPTURE_BOTH + + if not node.has_hash_bang(): + flags |= capture.CAPTURE_NEEDS_SHELL + + capture.set_flags(flags) + + # Get input text + input_type = node.input + output_type = node.output + + # Clear the panel + panel.clear() + + if output_type == 'output-panel': + panel.show() + + # Assign the error output to the output panel + panel.set_process(capture) + + if input_type != 'nothing' and view is not None: + if input_type == 'document': + start, end = document.get_bounds() + elif input_type == 'selection' or input_type == 'selection-document': + try: + start, end = document.get_selection_bounds() + except ValueError: + if input_type == 'selection-document': + start, end = document.get_bounds() + + if output_type == 'replace-selection': + document.select_range(start, end) + else: + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + + elif input_type == 'line': + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + if not start.starts_line(): + start.set_line_offset(0) + if not end.ends_line(): + end.forward_to_line_end() + elif input_type == 'word': + start = document.get_iter_at_mark(document.get_insert()) + end = start.copy() + if not start.inside_word(): + panel.write(_('You must be inside a word to run this command'), + panel.error_tag) + return + if not start.starts_word(): + start.backward_word_start() + if not end.ends_word(): + end.forward_word_end() + + input_text = document.get_text(start, end, False) + capture.set_input(input_text) + + # Assign the standard output to the chosen "file" + if output_type == 'new-document': + tab = window.create_tab(True) + view = tab.get_view() + document = tab.get_document() + pos = document.get_start_iter() + capture.connect('stdout-line', capture_stdout_line_document, document, pos) + document.begin_user_action() + view.set_editable(False) + view.set_cursor_visible(False) + elif output_type != 'output-panel' and output_type != 'nothing' and view is not None: + document.begin_user_action() + view.set_editable(False) + view.set_cursor_visible(False) + + if output_type.startswith('replace-'): + if output_type == 'replace-selection': + try: + start_iter, end_iter = document.get_selection_bounds() + except ValueError: + start_iter = document.get_iter_at_mark(document.get_insert()) + end_iter = start_iter.copy() + elif output_type == 'replace-document': + start_iter, end_iter = document.get_bounds() + capture.connect('stdout-line', capture_delayed_replace, + document, start_iter, end_iter) + else: + if output_type == 'insert': + pos = document.get_iter_at_mark(document.get_insert()) + else: + pos = document.get_end_iter() + capture.connect('stdout-line', capture_stdout_line_document, document, pos) + elif output_type != 'nothing': + capture.connect('stdout-line', capture_stdout_line_panel, panel) + + if not document is None: + document.begin_user_action() + + capture.connect('stderr-line', capture_stderr_line_panel, panel) + capture.connect('begin-execute', capture_begin_execute_panel, panel, view, node.name) + capture.connect('end-execute', capture_end_execute_panel, panel, view, output_type) + + # Run the command + capture.execute() + + if output_type != 'nothing': + if not document is None: + document.end_user_action() + +class MultipleDocumentsSaver: + def __init__(self, window, panel, all_docs, node): + self._window = window + self._panel = panel + self._node = node + + if all_docs: + docs = window.get_documents() + else: + docs = [window.get_active_document()] + + self._docs_to_save = [doc for doc in docs if doc.get_modified()] + self.save_next_document() + + def save_next_document(self): + if len(self._docs_to_save) == 0: + # The documents are saved, we can run the tool. + run_external_tool(self._window, self._panel, self._node) + else: + next_doc = self._docs_to_save[0] + self._docs_to_save.remove(next_doc) + + Gedit.commands_save_document_async(next_doc, + self._window, + None, + self.on_document_saved, + None) + + def on_document_saved(self, doc, result, user_data): + saved = Gedit.commands_save_document_finish(doc, result) + if saved: + self.save_next_document() + + +def capture_menu_action(action, parameter, window, panel, node): + if node.save_files == 'document' and window.get_active_document(): + MultipleDocumentsSaver(window, panel, False, node) + return + elif node.save_files == 'all': + MultipleDocumentsSaver(window, panel, True, node) + return + + run_external_tool(window, panel, node) + + +def capture_stderr_line_panel(capture, line, panel): + if not panel.visible(): + panel.show() + + panel.write(line, panel.error_tag) + + +def capture_begin_execute_panel(capture, panel, view, label): + if view: + view.get_window(Gtk.TextWindowType.TEXT).set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) + + panel['stop'].set_sensitive(True) + panel.clear() + panel.write(_("Running tool:"), panel.italic_tag) + panel.write(" %s\n\n" % label, panel.bold_tag) + + +def capture_end_execute_panel(capture, exit_code, panel, view, output_type): + panel['stop'].set_sensitive(False) + + if view: + if output_type in ('new-document', 'replace-document'): + doc = view.get_buffer() + start = doc.get_start_iter() + end = start.copy() + end.forward_chars(300) + uri = '' + + mtype, uncertain = Gio.content_type_guess(None, doc.get_text(start, end, False).encode('utf-8')) + lmanager = GtkSource.LanguageManager.get_default() + + location = doc.get_file().get_location() + if location: + uri = location.get_uri() + language = lmanager.guess_language(uri, mtype) + + if language is not None: + doc.set_language(language) + + view.get_window(Gtk.TextWindowType.TEXT).set_cursor(Gdk.Cursor.new(Gdk.CursorType.XTERM)) + view.set_cursor_visible(True) + view.set_editable(True) + + if exit_code == 0: + panel.write("\n" + _("Done.") + "\n", panel.italic_tag) + else: + panel.write("\n" + _("Exited") + ":", panel.italic_tag) + panel.write(" %d\n" % exit_code, panel.bold_tag) + + +def capture_stdout_line_panel(capture, line, panel): + panel.write(line) + + +def capture_stdout_line_document(capture, line, document, pos): + document.insert(pos, line) + + +def capture_delayed_replace(capture, line, document, start_iter, end_iter): + document.delete(start_iter, end_iter) + + # Must be done after deleting the text + pos = document.get_iter_at_mark(document.get_insert()) + + capture_stdout_line_document(capture, line, document, pos) + + capture.disconnect_by_func(capture_delayed_replace) + capture.connect('stdout-line', capture_stdout_line_document, document, pos) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/library.py b/plugins/externaltools/tools/library.py new file mode 100644 index 0000000..adfd943 --- /dev/null +++ b/plugins/externaltools/tools/library.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import re +import locale +import platform +from gi.repository import GLib + + +class Singleton(object): + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs) + cls._instance.__init_once__() + + return cls._instance + + +class ToolLibrary(Singleton): + def __init_once__(self): + self.locations = [] + + def set_locations(self, datadir): + self.locations = [] + + if platform.platform() != 'Windows': + for d in self.get_xdg_data_dirs(): + self.locations.append(os.path.join(d, 'gedit', 'plugins', 'externaltools', 'tools')) + + self.locations.append(datadir) + + # self.locations[0] is where we save the custom scripts + if platform.platform() == 'Windows': + toolsdir = os.path.expanduser('~/gedit/tools') + else: + userdir = os.getenv('GNOME22_USER_DIR') + if userdir: + toolsdir = os.path.join(userdir, 'gedit/tools') + else: + toolsdir = os.path.join(GLib.get_user_config_dir(), 'gedit/tools') + + self.locations.insert(0, toolsdir) + + if not os.path.isdir(self.locations[0]): + os.makedirs(self.locations[0]) + self.tree = ToolDirectory(self, '') + self.import_old_xml_store() + else: + self.tree = ToolDirectory(self, '') + + # cf. http://standards.freedesktop.org/basedir-spec/latest/ + def get_xdg_data_dirs(self): + dirs = os.getenv('XDG_DATA_DIRS') + if dirs: + dirs = dirs.split(os.pathsep) + else: + dirs = ('/usr/local/share', '/usr/share') + return dirs + + # This function is meant to be ran only once, when the tools directory is + # created. It imports eventual tools that have been saved in the old XML + # storage file. + def import_old_xml_store(self): + import xml.etree.ElementTree as et + userdir = os.getenv('GNOME22_USER_DIR') + if userdir: + filename = os.path.join(userdir, 'gedit/gedit-tools.xml') + else: + filename = os.path.join(GLib.get_user_config_dir(), 'gedit/gedit-tools.xml') + + if not os.path.isfile(filename): + return + + print("External tools: importing old tools into the new store...") + + xtree = et.parse(filename) + xroot = xtree.getroot() + + for xtool in xroot: + for i in self.tree.tools: + if i.name == xtool.get('label'): + tool = i + break + else: + tool = Tool(self.tree) + tool.name = xtool.get('label') + tool.autoset_filename() + self.tree.tools.append(tool) + tool.comment = xtool.get('description') + tool.shortcut = xtool.get('accelerator') + tool.applicability = xtool.get('applicability') + tool.output = xtool.get('output') + tool.input = xtool.get('input') + + tool.save_with_script(xtool.text) + + def get_full_path(self, path, mode='r', system=True, local=True): + assert (system or local) + if path is None: + return None + if mode == 'r': + if system and local: + locations = self.locations + elif local and not system: + locations = [self.locations[0]] + elif system and not local: + locations = self.locations[1:] + else: + raise ValueError("system and local can't be both set to False") + + for i in locations: + p = os.path.join(i, path) + if os.path.lexists(p): + return p + return None + else: + path = os.path.join(self.locations[0], path) + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + os.mkdir(dirname) + return path + + +class ToolDirectory(object): + def __init__(self, parent, dirname): + super(ToolDirectory, self).__init__() + self.subdirs = list() + self.tools = list() + if isinstance(parent, ToolDirectory): + self.parent = parent + self.library = parent.library + else: + self.parent = None + self.library = parent + self.dirname = dirname + self._load() + + def listdir(self): + elements = dict() + for l in self.library.locations: + d = os.path.join(l, self.dirname) + if not os.path.isdir(d): + continue + for i in os.listdir(d): + elements[i] = None + keys = sorted(elements.keys()) + return keys + + def _load(self): + for p in self.listdir(): + path = os.path.join(self.dirname, p) + full_path = self.library.get_full_path(path) + if os.path.isdir(full_path): + self.subdirs.append(ToolDirectory(self, p)) + elif os.path.isfile(full_path) and os.access(full_path, os.X_OK): + self.tools.append(Tool(self, p)) + + def get_path(self): + if self.parent is None: + return self.dirname + else: + return os.path.join(self.parent.get_path(), self.dirname) + path = property(get_path) + + def get_name(self): + return os.path.basename(self.dirname) + name = property(get_name) + + def delete_tool(self, tool): + # Only remove it if it lays in $HOME + if tool in self.tools: + path = tool.get_path() + if path is not None: + filename = os.path.join(self.library.locations[0], path) + if os.path.isfile(filename): + os.unlink(filename) + self.tools.remove(tool) + return True + else: + return False + + def revert_tool(self, tool): + # Only remove it if it lays in $HOME + filename = os.path.join(self.library.locations[0], tool.get_path()) + if tool in self.tools and os.path.isfile(filename): + os.unlink(filename) + tool._load() + return True + else: + return False + + +class Tool(object): + RE_KEY = re.compile('^([a-zA-Z_][a-zA-Z0-9_.\-]*)(\[([a-zA-Z_@]+)\])?$') + + def __init__(self, parent, filename=None): + super(Tool, self).__init__() + self.parent = parent + self.library = parent.library + self.filename = filename + self.changed = False + self._properties = dict() + self._transform = { + 'Languages': [self._to_list, self._from_list] + } + self._load() + + def _to_list(self, value): + if value.strip() == '': + return [] + else: + return [x.strip() for x in value.split(',')] + + def _from_list(self, value): + return ','.join(value) + + def _parse_value(self, key, value): + if key in self._transform: + return self._transform[key][0](value) + else: + return value + + def _load(self): + if self.filename is None: + return + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return + + fp = open(filename, 'r', 1, encoding='utf-8') + in_block = False + lang = locale.getlocale(locale.LC_MESSAGES)[0] + + for line in fp: + if not in_block: + in_block = line.startswith('# [Gedit Tool]') + continue + if line.startswith('##') or line.startswith('# #'): + continue + if not line.startswith('# '): + break + + try: + (key, value) = [i.strip() for i in line[2:].split('=', 1)] + m = self.RE_KEY.match(key) + if m.group(3) is None: + self._properties[m.group(1)] = self._parse_value(m.group(1), value) + elif lang is not None and lang.startswith(m.group(3)): + self._properties[m.group(1)] = self._parse_value(m.group(1), value) + except ValueError: + break + fp.close() + self.changed = False + + def _set_property_if_changed(self, key, value): + if value != self._properties.get(key): + self._properties[key] = value + + self.changed = True + + def is_global(self): + return self.library.get_full_path(self.get_path(), local=False) is not None + + def is_local(self): + return self.library.get_full_path(self.get_path(), system=False) is not None + + def is_global(self): + return self.library.get_full_path(self.get_path(), local=False) is not None + + def get_path(self): + if self.filename is not None: + return os.path.join(self.parent.get_path(), self.filename) + else: + return None + path = property(get_path) + + # This command is the one that is meant to be ran + # (later, could have an Exec key or something) + def get_command(self): + return self.library.get_full_path(self.get_path()) + command = property(get_command) + + def get_applicability(self): + applicability = self._properties.get('Applicability') + if applicability: + return applicability + return 'all' + + def set_applicability(self, value): + self._set_property_if_changed('Applicability', value) + + applicability = property(get_applicability, set_applicability) + + def get_name(self): + name = self._properties.get('Name') + if name: + return name + return os.path.basename(self.filename) + + def set_name(self, value): + self._set_property_if_changed('Name', value) + + name = property(get_name, set_name) + + def get_shortcut(self): + shortcut = self._properties.get('Shortcut') + if shortcut: + return shortcut + return None + + def set_shortcut(self, value): + self._set_property_if_changed('Shortcut', value) + + shortcut = property(get_shortcut, set_shortcut) + + def get_comment(self): + comment = self._properties.get('Comment') + if comment: + return comment + return self.filename + + def set_comment(self, value): + self._set_property_if_changed('Comment', value) + + comment = property(get_comment, set_comment) + + def get_input(self): + input = self._properties.get('Input') + if input: + return input + return 'nothing' + + def set_input(self, value): + self._set_property_if_changed('Input', value) + + input = property(get_input, set_input) + + def get_output(self): + output = self._properties.get('Output') + if output: + return output + return 'output-panel' + + def set_output(self, value): + self._set_property_if_changed('Output', value) + + output = property(get_output, set_output) + + def get_save_files(self): + save_files = self._properties.get('Save-files') + if save_files: + return save_files + return 'nothing' + + def set_save_files(self, value): + self._set_property_if_changed('Save-files', value) + + save_files = property(get_save_files, set_save_files) + + def get_languages(self): + languages = self._properties.get('Languages') + if languages: + return languages + return [] + + def set_languages(self, value): + self._set_property_if_changed('Languages', value) + + languages = property(get_languages, set_languages) + + def has_hash_bang(self): + if self.filename is None: + return True + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return True + + fp = open(filename, 'r', 1, encoding='utf-8') + for line in fp: + if line.strip() == '': + continue + return line.startswith('#!') + + # There is no property for this one because this function is quite + # expensive to perform + def get_script(self): + if self.filename is None: + return ["#!/bin/sh\n"] + + filename = self.library.get_full_path(self.get_path()) + if filename is None: + return ["#!/bin/sh\n"] + + fp = open(filename, 'r', 1, encoding='utf-8') + lines = list() + + # before entering the data block + for line in fp: + if line.startswith('# [Gedit Tool]'): + break + lines.append(line) + # in the block: + for line in fp: + if line.startswith('##'): + continue + if not (line.startswith('# ') and '=' in line): + # after the block: strip one emtpy line (if present) + if line.strip() != '': + lines.append(line) + break + # after the block + for line in fp: + lines.append(line) + fp.close() + return lines + + def _dump_properties(self): + lines = ['# [Gedit Tool]'] + for item in self._properties.items(): + if item[0] in self._transform: + lines.append('# %s=%s' % (item[0], self._transform[item[0]][1](item[1]))) + elif item[1] is not None: + lines.append('# %s=%s' % item) + return '\n'.join(lines) + '\n' + + def save_with_script(self, script): + filename = self.library.get_full_path(self.filename, 'w') + fp = open(filename, 'w', 1, encoding='utf-8') + + # Make sure to first print header (shebang, modeline), then + # properties, and then actual content + header = [] + content = [] + inheader = True + + # Parse + for line in script: + line = line.rstrip("\n") + if not inheader: + content.append(line) + elif line.startswith('#!'): + # Shebang (should be always present) + header.append(line) + elif line.strip().startswith('#') and ('-*-' in line or 'ex:' in line or 'vi:' in line or 'vim:' in line): + header.append(line) + else: + content.append(line) + inheader = False + + # Write out header + for line in header: + fp.write(line + "\n") + + fp.write(self._dump_properties()) + fp.write("\n") + + for line in content: + fp.write(line + "\n") + + fp.close() + os.chmod(filename, 0o750) + self.changed = False + + def save(self): + if self.changed: + self.save_with_script(self.get_script()) + + def autoset_filename(self): + if self.filename is not None: + return + dirname = self.parent.path + if dirname != '': + dirname += os.path.sep + + basename = self.name.lower().replace(' ', '-').replace('/', '-') + + if self.library.get_full_path(dirname + basename): + i = 2 + while self.library.get_full_path(dirname + "%s-%d" % (basename, i)): + i += 1 + basename = "%s-%d" % (basename, i) + self.filename = basename + +if __name__ == '__main__': + library = ToolLibrary() + library.set_locations(os.path.expanduser("~/.config/gedit/tools")) + + def print_tool(t, indent): + print(indent * " " + "%s: %s" % (t.filename, t.name)) + + def print_dir(d, indent): + print(indent * " " + d.dirname + '/') + for i in d.subdirs: + print_dir(i, indent + 1) + for i in d.tools: + print_tool(i, indent + 1) + + print_dir(library.tree, 0) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/linkparsing.py b/plugins/externaltools/tools/linkparsing.py new file mode 100644 index 0000000..d9c09a5 --- /dev/null +++ b/plugins/externaltools/tools/linkparsing.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2010 Per Arneng +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import re + + +class Link: + """ + This class represents a file link from within a string given by the + output of some software tool. A link contains a reference to a file, the + line number within the file and the boundaries within the given output + string that should be marked as a link. + """ + + def __init__(self, path, line_nr, col_nr, start, end): + """ + path -- the path of the file (that could be extracted) + line_nr -- the line nr of the specified file + col_nr -- the col nr of the specific file + start -- the index within the string that the link starts at + end -- the index within the string where the link ends at + """ + self.path = path + self.line_nr = int(line_nr) + self.col_nr = int(col_nr) + self.start = start + self.end = end + + def __repr__(self): + return "%s[%s][%s](%s:%s)" % (self.path, self.line_nr, self.col_nr, + self.start, self.end) + + +class LinkParser: + """ + Parses a text using different parsing providers with the goal of finding one + or more file links within the text. A typical example could be the output + from a compiler that specifies an error in a specific file. The path of the + file, the line nr and some more info is then returned so that it can be used + to be able to navigate from the error output in to the specific file. + + The actual work of parsing the text is done by instances of classes that + inherits from AbstractLinkParser or by regular expressions. To add a new + parser just create a class that inherits from AbstractLinkParser and then + register in this class cunstructor using the method add_parser. If you want + to add a regular expression then just call add_regexp in this class + constructor and provide your regexp string as argument. + """ + + def __init__(self): + self._providers = [] + self.add_regexp(REGEXP_STANDARD) + self.add_regexp(REGEXP_PYTHON) + self.add_regexp(REGEXP_VALAC) + self.add_regexp(REGEXP_BASH) + self.add_regexp(REGEXP_RUBY) + self.add_regexp(REGEXP_PERL) + self.add_regexp(REGEXP_MCS) + + def add_parser(self, parser): + self._providers.append(parser) + + def add_regexp(self, regexp): + """ + Adds a regular expression string that should match a link using + re.MULTILINE and re.VERBOSE regexp. The area marked as a link should + be captured by a group named lnk. The path of the link should be + captured by a group named pth. The line number should be captured by + a group named ln. To read more about this look at the documentation + for the RegexpLinkParser constructor. + """ + self.add_parser(RegexpLinkParser(regexp)) + + def parse(self, text): + """ + Parses the given text and returns a list of links that are parsed from + the text. This method delegates to parser providers that can parse + output from different kinds of formats. If no links are found then an + empty list is returned. + + text -- the text to scan for file links. 'text' can not be None. + """ + if text is None: + raise ValueError("text can not be None") + + links = [] + + for provider in self._providers: + links.extend(provider.parse(text)) + + return links + + +class AbstractLinkParser(object): + """The "abstract" base class for link parses""" + + def parse(self, text): + """ + This method should be implemented by subclasses. It takes a text as + argument (never None) and then returns a list of Link objects. If no + links are found then an empty list is expected. The Link class is + defined in this module. If you do not override this method then a + NotImplementedError will be thrown. + + text -- the text to parse. This argument is never None. + """ + raise NotImplementedError("need to implement a parse method") + + +class RegexpLinkParser(AbstractLinkParser): + """ + A class that represents parsers that only use one single regular expression. + It can be used by subclasses or by itself. See the constructor documentation + for details about the rules surrouning the regexp. + """ + + def __init__(self, regex): + """ + Creates a new RegexpLinkParser based on the given regular expression. + The regular expression is multiline and verbose (se python docs on + compilation flags). The regular expression should contain three named + capturing groups 'lnk', 'pth' and 'ln'. 'lnk' represents the area wich + should be marked as a link in the text. 'pth' is the path that should + be looked for and 'ln' is the line number in that file. + """ + self.re = re.compile(regex, re.MULTILINE | re.VERBOSE) + + def parse(self, text): + links = [] + for m in re.finditer(self.re, text): + groups = m.groups() + + path = m.group("pth") + line_nr = m.group("ln") + start = m.start("lnk") + end = m.end("lnk") + + # some regexes may have a col group + if len(groups) > 3 and groups[3] != None: + col_nr = m.group("col") + else: + col_nr = 0 + + link = Link(path, line_nr, col_nr, start, end) + links.append(link) + + return links + +# gcc 'test.c:13: warning: ...' +# grep 'test.c:5:int main(...' +# javac 'Test.java:13: ...' +# ruby 'test.rb:5: ...' +# scalac 'Test.scala:5: ...' +# sbt (scala) '[error] test.scala:4: ...' +# 6g (go) 'test.go:9: ...' +REGEXP_STANDARD = r""" +^ +(?:\[(?:error|warn)\]\ )? +(?P + (?P [^ \:\n]* ) + \: + (?P \d+) + \:? + (?P \d+)? +) +\:""" + +# python ' File "test.py", line 13' +REGEXP_PYTHON = r""" +^\s\sFile\s +(?P + \" + (?P [^\"]+ ) + \",\sline\s + (?P \d+ ) +)""" + +# python 'test.sh: line 5:' +REGEXP_BASH = r""" +^(?P + (?P .* ) + \:\sline\s + (?P \d+ ) +)\:""" + +# valac 'Test.vala:13.1-13.3: ...' +REGEXP_VALAC = r""" +^(?P + (?P + .*vala + ) + \: + (?P + \d+ + ) + \.\d+-\d+\.\d+ + )\: """ + +#ruby +#test.rb:5: ... +# from test.rb:3:in `each' +# fist line parsed by REGEXP_STANDARD +REGEXP_RUBY = r""" +^\s+from\s +(?P + (?P + .* + ) + \: + (?P + \d+ + ) + )""" + +# perl 'syntax error at test.pl line 88, near "$fake_var' +REGEXP_PERL = r""" +\sat\s +(?P + (?P .* ) + \sline\s + (?P \d+ ) +)""" + +# mcs (C#) 'Test.cs(12,7): error CS0103: The name `fakeMethod' +# fpc (Pascal) 'hello.pas(11,1) Fatal: Syntax error, ":" expected but "BEGIN"' +REGEXP_MCS = r""" +^ +(?P + (?P \S+ ) + \( + (?P \d+ ) + ,\d+\) +) +\:?\s +""" + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/manager.py b/plugins/externaltools/tools/manager.py new file mode 100644 index 0000000..072286b --- /dev/null +++ b/plugins/externaltools/tools/manager.py @@ -0,0 +1,878 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +__all__ = ('Manager', ) + +import os.path +from .library import * +from .functions import * +import hashlib +from xml.sax import saxutils +from gi.repository import Gio, GObject, Gtk, GtkSource, Gedit + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +class LanguagesPopup(Gtk.Popover): + __gtype_name__ = "LanguagePopup" + + COLUMN_NAME = 0 + COLUMN_ID = 1 + COLUMN_ENABLED = 2 + + def __init__(self, widget, languages): + Gtk.Popover.__init__(self, relative_to=widget) + + self.props.can_focus = True + + self.build() + self.init_languages(languages) + + self.view.get_selection().select_path((0,)) + + def build(self): + self.model = Gtk.ListStore(str, str, bool) + + self.sw = Gtk.ScrolledWindow() + self.sw.set_size_request(-1, 200) + self.sw.show() + + self.sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + self.sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + + self.view = Gtk.TreeView(model=self.model) + self.view.show() + + self.view.set_headers_visible(False) + + column = Gtk.TreeViewColumn() + + renderer = Gtk.CellRendererToggle() + column.pack_start(renderer, False) + column.add_attribute(renderer, 'active', self.COLUMN_ENABLED) + + renderer.connect('toggled', self.on_language_toggled) + + renderer = Gtk.CellRendererText() + column.pack_start(renderer, True) + column.add_attribute(renderer, 'text', self.COLUMN_NAME) + + self.view.append_column(column) + self.view.set_row_separator_func(self.on_separator, None) + + self.sw.add(self.view) + + self.add(self.sw) + + def enabled_languages(self, model, path, piter, ret): + enabled = model.get_value(piter, self.COLUMN_ENABLED) + + if path.get_indices()[0] == 0 and enabled: + return True + + if enabled: + ret.append(model.get_value(piter, self.COLUMN_ID)) + + return False + + def languages(self): + ret = [] + + self.model.foreach(self.enabled_languages, ret) + return ret + + def on_separator(self, model, piter, user_data=None): + val = model.get_value(piter, self.COLUMN_NAME) + return val == '-' + + def init_languages(self, languages): + manager = GtkSource.LanguageManager() + langs = [manager.get_language(x) for x in manager.get_language_ids()] + langs.sort(key=lambda x: x.get_name()) + + self.model.append([_('All languages'), None, not languages]) + self.model.append(['-', None, False]) + self.model.append([_('Plain Text'), 'plain', 'plain' in languages]) + self.model.append(['-', None, False]) + + for lang in langs: + self.model.append([lang.get_name(), lang.get_id(), lang.get_id() in languages]) + + def correct_all(self, model, path, piter, enabled): + if path.get_indices()[0] == 0: + return False + + model.set_value(piter, self.COLUMN_ENABLED, enabled) + + def on_language_toggled(self, renderer, path): + piter = self.model.get_iter(path) + + enabled = self.model.get_value(piter, self.COLUMN_ENABLED) + self.model.set_value(piter, self.COLUMN_ENABLED, not enabled) + + if path == '0': + self.model.foreach(self.correct_all, False) + else: + self.model.set_value(self.model.get_iter_first(), self.COLUMN_ENABLED, False) + + +class Manager(GObject.Object): + TOOL_COLUMN = 0 # For Tree + NAME_COLUMN = 1 # For Combo + + __gsignals__ = { + 'tools-updated': (GObject.SignalFlags.RUN_LAST, None, ()) + } + + def __init__(self, datadir): + GObject.Object.__init__(self) + self.datadir = datadir + self.dialog = None + self._size = (0, 0) + self._languages = {} + self._tool_rows = {} + + self.build() + + def get_final_size(self): + return self._size + + def build(self): + callbacks = { + 'on_add_tool_button_clicked': self.on_add_tool_button_clicked, + 'on_remove_tool_button_clicked': self.on_remove_tool_button_clicked, + 'on_tool_manager_dialog_delete_event': self.on_tool_manager_dialog_delete_event, + 'on_tool_manager_dialog_focus_out': self.on_tool_manager_dialog_focus_out, + 'on_tool_manager_dialog_configure_event': self.on_tool_manager_dialog_configure_event, + 'on_accelerator_key_press': self.on_accelerator_key_press, + 'on_accelerator_focus_in': self.on_accelerator_focus_in, + 'on_accelerator_focus_out': self.on_accelerator_focus_out, + 'on_accelerator_backspace': self.on_accelerator_backspace, + 'on_applicability_changed': self.on_applicability_changed, + 'on_languages_button_clicked': self.on_languages_button_clicked + } + + # Load the "main-window" widget from the ui file. + self.ui = Gtk.Builder() + self.ui.add_from_file(os.path.join(self.datadir, 'ui', 'tools.ui')) + self.ui.connect_signals(callbacks) + self.dialog = self.ui.get_object('tool-manager-dialog') + + self.view = self['view'] + + self.__init_tools_model() + self.__init_tools_view() + + # join treeview and toolbar + context = self['scrolled_window1'].get_style_context() + context.set_junction_sides(Gtk.JunctionSides.BOTTOM) + context = self['toolbar1'].get_style_context() + context.set_junction_sides(Gtk.JunctionSides.TOP) + context.set_junction_sides(Gtk.JunctionSides.BOTTOM) + + for name in ['input', 'output', 'applicability', 'save-files']: + self.__init_combobox(name) + + self.do_update() + + def expand_from_doc(self, doc): + row = None + + if doc: + if doc.get_language(): + lid = doc.get_language().get_id() + + if lid in self._languages: + row = self._languages[lid] + elif 'plain' in self._languages: + row = self._languages['plain'] + + if not row and None in self._languages: + row = self._languages[None] + + if not row: + return + + self.view.expand_row(row.get_path(), False) + self.view.get_selection().select_path(row.get_path()) + + def run(self, window): + if self.dialog is None: + self.build() + + # Open up language + self.expand_from_doc(window.get_active_document()) + + self.dialog.set_transient_for(window) + window.get_group().add_window(self.dialog) + self.dialog.present() + + def add_accelerator(self, item): + if not item.shortcut: + return + + if item.shortcut in self.accelerators: + if not item in self.accelerators[item.shortcut]: + self.accelerators[item.shortcut].append(item) + else: + self.accelerators[item.shortcut] = [item] + + def remove_accelerator(self, item, shortcut=None): + if not shortcut: + shortcut = item.shortcut + + if not shortcut in self.accelerators: + return + + self.accelerators[shortcut].remove(item) + + if not self.accelerators[shortcut]: + del self.accelerators[shortcut] + + def add_tool_to_language(self, tool, language): + if isinstance(language, GtkSource.Language): + lid = language.get_id() + else: + lid = language + + if not lid in self._languages: + piter = self.model.append(None, [language]) + + parent = Gtk.TreeRowReference.new(self.model, self.model.get_path(piter)) + self._languages[lid] = parent + else: + parent = self._languages[lid] + + piter = self.model.get_iter(parent.get_path()) + child = self.model.append(piter, [tool]) + + if not tool in self._tool_rows: + self._tool_rows[tool] = [] + + self._tool_rows[tool].append(Gtk.TreeRowReference.new(self.model, self.model.get_path(child))) + return child + + def add_tool(self, tool): + manager = GtkSource.LanguageManager() + ret = None + + for lang in tool.languages: + l = manager.get_language(lang) + + if l: + ret = self.add_tool_to_language(tool, l) + elif lang == 'plain': + ret = self.add_tool_to_language(tool, 'plain') + + if not ret: + ret = self.add_tool_to_language(tool, None) + + self.add_accelerator(tool) + return ret + + def __init_tools_model(self): + self.tools = ToolLibrary() + self.current_node = None + self.script_hash = None + self.accelerators = dict() + + self.model = Gtk.TreeStore(object) + self.view.set_model(self.model) + + for tool in self.tools.tree.tools: + self.add_tool(tool) + + self.model.set_default_sort_func(self.sort_tools) + self.model.set_sort_column_id(-1, Gtk.SortType.ASCENDING) + + def sort_tools(self, model, iter1, iter2, user_data=None): + # For languages, sort All before everything else, otherwise alphabetical + t1 = model.get_value(iter1, self.TOOL_COLUMN) + t2 = model.get_value(iter2, self.TOOL_COLUMN) + + if model.iter_parent(iter1) is None: + if t1 is None: + return -1 + + if t2 is None: + return 1 + + def lang_name(lang): + if isinstance(lang, GtkSource.Language): + return lang.get_name() + else: + return _('Plain Text') + + n1 = lang_name(t1) + n2 = lang_name(t2) + else: + n1 = t1.name + n2 = t2.name + + if n1.lower() < n2.lower(): + return -1 + elif n1.lower() > n2.lower(): + return 1 + else: + return 0 + + def __init_tools_view(self): + # Tools column + column = Gtk.TreeViewColumn('Tools') + renderer = Gtk.CellRendererText() + column.pack_start(renderer, False) + renderer.set_property('editable', True) + self.view.append_column(column) + + column.set_cell_data_func(renderer, self.get_cell_data_cb, None) + + renderer.connect('edited', self.on_view_label_cell_edited) + renderer.connect('editing-started', self.on_view_label_cell_editing_started) + + self.selection_changed_id = self.view.get_selection().connect('changed', self.on_view_selection_changed, None) + + def __init_combobox(self, name): + combo = self[name] + combo.set_active(0) + + # Convenience function to get an object from its name + def __getitem__(self, key): + return self.ui.get_object(key) + + def set_active_by_name(self, combo_name, option_name): + combo = self[combo_name] + model = combo.get_model() + piter = model.get_iter_first() + while piter is not None: + if model.get_value(piter, self.NAME_COLUMN) == option_name: + combo.set_active_iter(piter) + return True + piter = model.iter_next(piter) + return False + + def get_selected_tool(self): + model, piter = self.view.get_selection().get_selected() + + if piter is not None: + tool = model.get_value(piter, self.TOOL_COLUMN) + + if not isinstance(tool, Tool): + tool = None + + return piter, tool + else: + return None, None + + def compute_hash(self, string): + return hashlib.md5(string.encode('utf-8')).hexdigest() + + def save_current_tool(self): + if self.current_node is None: + return + + if self.current_node.filename is None: + self.current_node.autoset_filename() + + def combo_value(o, name): + combo = o[name] + return combo.get_model().get_value(combo.get_active_iter(), self.NAME_COLUMN) + + self.current_node.input = combo_value(self, 'input') + self.current_node.output = combo_value(self, 'output') + self.current_node.applicability = combo_value(self, 'applicability') + self.current_node.save_files = combo_value(self, 'save-files') + + buf = self['commands'].get_buffer() + (start, end) = buf.get_bounds() + script = buf.get_text(start, end, False) + h = self.compute_hash(script) + if h != self.script_hash: + # script has changed -> save it + self.current_node.save_with_script([line + "\n" for line in script.splitlines()]) + self.script_hash = h + else: + self.current_node.save() + + self.update_remove_revert() + + def clear_fields(self): + self['accelerator'].set_text('') + + buf = self['commands'].get_buffer() + buf.begin_not_undoable_action() + buf.set_text('') + buf.end_not_undoable_action() + + for nm in ('input', 'output', 'applicability', 'save-files'): + self[nm].set_active(0) + + self['languages_label'].set_text(_('All Languages')) + + def fill_languages_button(self): + if not self.current_node or not self.current_node.languages: + self['languages_label'].set_text(_('All Languages')) + else: + manager = GtkSource.LanguageManager() + langs = [] + + for lang in self.current_node.languages: + if lang == 'plain': + langs.append(_('Plain Text')) + else: + l = manager.get_language(lang) + + if l: + langs.append(l.get_name()) + + self['languages_label'].set_text(', '.join(langs)) + + def fill_fields(self): + self.update_accelerator_label() + + buf = self['commands'].get_buffer() + script = default(''.join(self.current_node.get_script()), '') + + buf.begin_not_undoable_action() + buf.set_text(script) + buf.end_not_undoable_action() + + self.script_hash = self.compute_hash(script) + + contenttype, uncertain = Gio.content_type_guess(None, script.encode('utf-8')) + lmanager = GtkSource.LanguageManager.get_default() + language = lmanager.guess_language(None, contenttype) + + if language is not None: + buf.set_language(language) + buf.set_highlight_syntax(True) + else: + buf.set_highlight_syntax(False) + + for nm in ('input', 'output', 'applicability', 'save-files'): + model = self[nm].get_model() + piter = model.get_iter_first() + self.set_active_by_name(nm, + default(self.current_node.__getattribute__(nm.replace('-', '_')), + model.get_value(piter, self.NAME_COLUMN))) + + self.fill_languages_button() + + def update_accelerator_label(self): + if self.current_node.shortcut: + key, mods = Gtk.accelerator_parse(self.current_node.shortcut) + label = Gtk.accelerator_get_label(key, mods) + self['accelerator'].set_text(label) + else: + self['accelerator'].set_text('') + + def update_remove_revert(self): + piter, node = self.get_selected_tool() + + removable = node is not None and node.is_local() + + self['remove-tool-button'].set_sensitive(removable) + self['revert-tool-button'].set_sensitive(removable) + + if node is not None and node.is_global(): + self['remove-tool-button'].hide() + self['revert-tool-button'].show() + else: + self['remove-tool-button'].show() + self['revert-tool-button'].hide() + + def do_update(self): + self.update_remove_revert() + + piter, node = self.get_selected_tool() + self.current_node = node + + if node is not None: + self.fill_fields() + self['tool-grid'].set_sensitive(True) + else: + self.clear_fields() + self['tool-grid'].set_sensitive(False) + + def language_id_from_iter(self, piter): + if not piter: + return None + + tool = self.model.get_value(piter, self.TOOL_COLUMN) + + if isinstance(tool, Tool): + piter = self.model.iter_parent(piter) + tool = self.model.get_value(piter, self.TOOL_COLUMN) + + if isinstance(tool, GtkSource.Language): + return tool.get_id() + elif tool: + return 'plain' + + return None + + def selected_language_id(self): + # Find current language if there is any + model, piter = self.view.get_selection().get_selected() + + return self.language_id_from_iter(piter) + + def on_add_tool_button_clicked(self, button): + self.save_current_tool() + + # block handlers while inserting a new item + self.view.get_selection().handler_block(self.selection_changed_id) + + self.current_node = Tool(self.tools.tree); + self.current_node.name = _('New tool') + self.tools.tree.tools.append(self.current_node) + + lang = self.selected_language_id() + + if lang: + self.current_node.languages = [lang] + + piter = self.add_tool(self.current_node) + + self.view.set_cursor(self.model.get_path(piter), + self.view.get_column(self.TOOL_COLUMN), + True) + self.fill_fields() + + self['tool-grid'].set_sensitive(True) + self.view.get_selection().handler_unblock(self.selection_changed_id) + + def tool_changed(self, tool, refresh=False): + for row in self._tool_rows[tool]: + self.model.set_value(self.model.get_iter(row.get_path()), + self.TOOL_COLUMN, + tool) + + if refresh and tool == self.current_node: + self.fill_fields() + + self.update_remove_revert() + + def on_remove_tool_button_clicked(self, button): + piter, node = self.get_selected_tool() + + if not node: + return + + if node.is_global(): + shortcut = node.shortcut + + if node.parent.revert_tool(node): + self.remove_accelerator(node, shortcut) + self.add_accelerator(node) + + self['revert-tool-button'].set_sensitive(False) + self.fill_fields() + + self.tool_changed(node) + else: + parent = self.model.iter_parent(piter) + language = self.language_id_from_iter(parent) + + self.model.remove(piter) + + if language in node.languages: + node.languages.remove(language) + + self._tool_rows[node] = [x for x in self._tool_rows[node] if x.valid()] + + if not self._tool_rows[node]: + del self._tool_rows[node] + + if node.parent.delete_tool(node): + self.remove_accelerator(node) + self.current_node = None + self.script_hash = None + + if self.model.iter_is_valid(piter): + self.view.set_cursor(self.model.get_path(piter), + self.view.get_column(self.TOOL_COLUMN), + False) + + self.view.grab_focus() + + path = self._languages[language].get_path() + parent = self.model.get_iter(path) + + if not self.model.iter_has_child(parent): + self.model.remove(parent) + del self._languages[language] + + def on_view_label_cell_edited(self, cell, path, new_text): + if new_text != '': + piter = self.model.get_iter(path) + tool = self.model.get_value(piter, self.TOOL_COLUMN) + + tool.name = new_text + + self.save_current_tool() + self.tool_changed(tool) + + def on_view_label_cell_editing_started(self, renderer, editable, path): + piter = self.model.get_iter(path) + tool = self.model.get_value(piter, self.TOOL_COLUMN) + + if isinstance(editable, Gtk.Entry): + editable.set_text(tool.name) + editable.grab_focus() + + def on_view_selection_changed(self, selection, userdata): + self.save_current_tool() + self.do_update() + + def accelerator_collision(self, name, node): + if not name in self.accelerators: + return [] + + ret = [] + + for other in self.accelerators[name]: + if not other.languages or not node.languages: + ret.append(other) + continue + + for lang in other.languages: + if lang in node.languages: + ret.append(other) + continue + + return ret + + def set_accelerator(self, keyval, mod): + # Check whether accelerator already exists + self.remove_accelerator(self.current_node) + + name = Gtk.accelerator_name(keyval, mod) + + if name == '': + self.current_node.shorcut = None + self.save_current_tool() + return True + + col = self.accelerator_collision(name, self.current_node) + + if col: + dialog = Gtk.MessageDialog(self.dialog, + Gtk.DialogFlags.MODAL, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + _('This accelerator is already bound to %s') % (', '.join(map(lambda x: x.name, col)),)) + + dialog.run() + dialog.destroy() + + self.add_accelerator(self.current_node) + return False + + self.current_node.shortcut = name + self.add_accelerator(self.current_node) + self.save_current_tool() + + return True + + def on_accelerator_key_press(self, entry, event): + mask = event.state & Gtk.accelerator_get_default_mod_mask() + + if event.keyval == Gdk.KEY_Escape: + self.update_accelerator_label() + self['commands'].grab_focus() + return True + elif event.keyval == Gdk.KEY_BackSpace: + return False + elif event.keyval in range(Gdk.KEY_F1, Gdk.KEY_F12 + 1): + # New accelerator + if self.set_accelerator(event.keyval, mask): + self.update_accelerator_label() + self['commands'].grab_focus() + + # Capture all `normal characters` + return True + elif Gdk.keyval_to_unicode(event.keyval): + if mask: + # New accelerator + if self.set_accelerator(event.keyval, mask): + self.update_accelerator_label() + self['commands'].grab_focus() + # Capture all `normal characters` + return True + else: + return False + + def on_accelerator_focus_in(self, entry, event): + if self.current_node is None: + return + if self.current_node.shortcut: + entry.set_text(_('Type a new accelerator, or press Backspace to clear')) + else: + entry.set_text(_('Type a new accelerator')) + + def on_accelerator_focus_out(self, entry, event): + if self.current_node is not None: + self.update_accelerator_label() + self.tool_changed(self.current_node) + + def on_accelerator_backspace(self, entry): + entry.set_text('') + self.remove_accelerator(self.current_node) + self.current_node.shortcut = None + self['commands'].grab_focus() + + def on_tool_manager_dialog_delete_event(self, dialog, event): + self.save_current_tool() + return False + + def on_tool_manager_dialog_focus_out(self, dialog, event): + self.save_current_tool() + self.emit('tools-updated') + + def on_tool_manager_dialog_configure_event(self, dialog, event): + if dialog.get_realized(): + alloc = dialog.get_allocation() + self._size = (alloc.width, alloc.height) + + def on_applicability_changed(self, combo): + applicability = combo.get_model().get_value(combo.get_active_iter(), + self.NAME_COLUMN) + + if applicability == 'always': + if self.current_node is not None: + self.current_node.languages = [] + + self.fill_languages_button() + + self['languages_button'].set_sensitive(applicability != 'always') + + def get_cell_data_cb(self, column, cell, model, piter, user_data=None): + tool = model.get_value(piter, self.TOOL_COLUMN) + + if tool is None or not isinstance(tool, Tool): + if tool is None: + label = _('All Languages') + elif not isinstance(tool, GtkSource.Language): + label = _('Plain Text') + else: + label = tool.get_name() + + markup = saxutils.escape(label) + editable = False + else: + escaped = saxutils.escape(tool.name) + + if tool.shortcut: + key, mods = Gtk.accelerator_parse(tool.shortcut) + label = Gtk.accelerator_get_label(key, mods) + markup = '%s (%s)' % (escaped, label) + else: + markup = escaped + + editable = True + + cell.set_properties(markup=markup, editable=editable) + + def tool_in_language(self, tool, lang): + if not lang in self._languages: + return False + + ref = self._languages[lang] + parent = ref.get_path() + + for row in self._tool_rows[tool]: + path = row.get_path() + + if path.get_indices()[0] == parent.get_indices()[0]: + return True + + return False + + def update_languages(self, popup): + self.current_node.languages = popup.languages() + self.fill_languages_button() + + piter, node = self.get_selected_tool() + ret = None + + if node: + ref = Gtk.TreeRowReference.new(self.model, self.model.get_path(piter)) + + # Update languages, make sure to inhibit selection change stuff + self.view.get_selection().handler_block(self.selection_changed_id) + + # Remove all rows that are no longer + for row in list(self._tool_rows[self.current_node]): + piter = self.model.get_iter(row.get_path()) + language = self.language_id_from_iter(piter) + + if (not language and not self.current_node.languages) or \ + (language in self.current_node.languages): + continue + + # Remove from language + self.model.remove(piter) + self._tool_rows[self.current_node].remove(row) + + # If language is empty, remove it + parent = self.model.get_iter(self._languages[language].get_path()) + + if not self.model.iter_has_child(parent): + self.model.remove(parent) + del self._languages[language] + + # Now, add for any that are new + manager = GtkSource.LanguageManager() + + for lang in self.current_node.languages: + if not self.tool_in_language(self.current_node, lang): + l = manager.get_language(lang) + + if not l: + l = 'plain' + + self.add_tool_to_language(self.current_node, l) + + if not self.current_node.languages and not self.tool_in_language(self.current_node, None): + self.add_tool_to_language(self.current_node, None) + + # Check if we can still keep the current + if not ref or not ref.valid(): + # Change selection to first language + path = self._tool_rows[self.current_node][0].get_path() + piter = self.model.get_iter(path) + parent = self.model.iter_parent(piter) + + # Expand parent, select child and scroll to it + self.view.expand_row(self.model.get_path(parent), False) + self.view.get_selection().select_path(path) + self.view.set_cursor(path, self.view.get_column(self.TOOL_COLUMN), False) + + self.view.get_selection().handler_unblock(self.selection_changed_id) + + def on_languages_button_clicked(self, button): + popup = LanguagesPopup(button, self.current_node.languages) + popup.show() + popup.connect('closed', self.update_languages) + +# ex:et:ts=4: diff --git a/plugins/externaltools/tools/meson.build b/plugins/externaltools/tools/meson.build new file mode 100644 index 0000000..bd623cf --- /dev/null +++ b/plugins/externaltools/tools/meson.build @@ -0,0 +1,36 @@ +externaltools_sources = files( + '__init__.py', + 'appactivatable.py', + 'capture.py', + 'filelookup.py', + 'functions.py', + 'library.py', + 'linkparsing.py', + 'manager.py', + 'outputpanel.py', + 'windowactivatable.py', +) + +install_data( + externaltools_sources, + install_dir: join_paths( + pkglibdir, + 'plugins', + 'externaltools', + ) +) + +externaltools_data = files( + 'outputpanel.ui', + 'tools.ui', +) + +install_data( + externaltools_data, + install_dir: join_paths( + pkgdatadir, + 'plugins', + 'externaltools', + 'ui', + ) +) diff --git a/plugins/externaltools/tools/outputpanel.py b/plugins/externaltools/tools/outputpanel.py new file mode 100644 index 0000000..e9fc241 --- /dev/null +++ b/plugins/externaltools/tools/outputpanel.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# Copyright (C) 2010 Per Arneng +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +__all__ = ('OutputPanel', 'UniqueById') + +import os +from weakref import WeakKeyDictionary +from .capture import * +import re +from . import linkparsing +from . import filelookup +from gi.repository import GLib, Gio, Gdk, Gtk, Pango, Gedit + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +class UniqueById: + __shared_state = WeakKeyDictionary() + + def __init__(self, i): + if i in self.__class__.__shared_state: + self.__dict__ = self.__class__.__shared_state[i] + return True + else: + self.__class__.__shared_state[i] = self.__dict__ + return False + + def states(self): + return self.__class__.__shared_state + + +class OutputPanel(UniqueById): + def __init__(self, datadir, window): + if UniqueById.__init__(self, window): + return + + callbacks = { + 'on_stop_clicked': self.on_stop_clicked, + 'on_view_visibility_notify_event': self.on_view_visibility_notify_event, + 'on_view_motion_notify_event': self.on_view_motion_notify_event + } + + self.profile_settings = self.get_profile_settings() + self.profile_settings.connect("changed", self.font_changed) + self.system_settings = Gio.Settings.new("org.gnome.desktop.interface") + self.system_settings.connect("changed::monospace-font-name", self.font_changed) + + self.window = window + self.ui = Gtk.Builder() + self.ui.add_from_file(os.path.join(datadir, 'ui', 'outputpanel.ui')) + self.ui.connect_signals(callbacks) + self['view'].connect('button-press-event', self.on_view_button_press_event) + + self.panel = self["output-panel"] + self.font_changed() + + buffer = self['view'].get_buffer() + + self.normal_tag = buffer.create_tag('normal') + + self.error_tag = buffer.create_tag('error') + self.error_tag.set_property('foreground', 'red') + + self.italic_tag = buffer.create_tag('italic') + self.italic_tag.set_property('style', Pango.Style.OBLIQUE) + + self.bold_tag = buffer.create_tag('bold') + self.bold_tag.set_property('weight', Pango.Weight.BOLD) + + self.invalid_link_tag = buffer.create_tag('invalid_link') + + self.link_tag = buffer.create_tag('link') + self.link_tag.set_property('underline', Pango.Underline.SINGLE) + + self.link_cursor = Gdk.Cursor.new(Gdk.CursorType.HAND2) + self.normal_cursor = Gdk.Cursor.new(Gdk.CursorType.XTERM) + + self.process = None + + self.links = [] + + self.link_parser = linkparsing.LinkParser() + self.file_lookup = filelookup.FileLookup(window) + + def get_profile_settings(self): + #FIXME return either the gnome-terminal settings or the gedit one + return Gio.Settings.new("org.gnome.gedit.plugins.externaltools") + + def font_changed(self, settings=None, key=None): + if self.profile_settings.get_boolean("use-system-font"): + font = self.system_settings.get_string("monospace-font-name") + else: + font = self.profile_settings.get_string("font") + + font_desc = Pango.font_description_from_string(font) + + self["view"].override_font(font_desc) + + def set_process(self, process): + self.process = process + + def __getitem__(self, key): + # Convenience function to get an object from its name + return self.ui.get_object(key) + + def on_stop_clicked(self, widget, *args): + if self.process is not None: + self.write("\n" + _('Stopped.') + "\n", + self.italic_tag) + self.process.stop(-1) + + def scroll_to_end(self): + iter = self['view'].get_buffer().get_end_iter() + self['view'].scroll_to_iter(iter, 0.0, False, 0.5, 0.5) + return False # don't requeue this handler + + def clear(self): + self['view'].get_buffer().set_text("") + self.links = [] + + def visible(self): + panel = self.window.get_bottom_panel() + return panel.props.visible and panel.props.visible_child == self.panel + + def write(self, text, tag=None): + buffer = self['view'].get_buffer() + + end_iter = buffer.get_end_iter() + insert = buffer.create_mark(None, end_iter, True) + + if tag is None: + buffer.insert(end_iter, text) + else: + buffer.insert_with_tags(end_iter, text, tag) + + # find all links and apply the appropriate tag for them + links = self.link_parser.parse(text) + for lnk in links: + insert_iter = buffer.get_iter_at_mark(insert) + lnk.start = insert_iter.get_offset() + lnk.start + lnk.end = insert_iter.get_offset() + lnk.end + + start_iter = buffer.get_iter_at_offset(lnk.start) + end_iter = buffer.get_iter_at_offset(lnk.end) + + tag = None + + # if the link points to an existing file then it is a valid link + if self.file_lookup.lookup(lnk.path) is not None: + self.links.append(lnk) + tag = self.link_tag + else: + tag = self.invalid_link_tag + + buffer.apply_tag(tag, start_iter, end_iter) + + buffer.delete_mark(insert) + GLib.idle_add(self.scroll_to_end) + + def show(self): + panel = self.window.get_bottom_panel() + panel.props.visible_child = self.panel + panel.show() + + def update_cursor_style(self, view, x, y): + if self.get_link_at_location(view, x, y) is not None: + cursor = self.link_cursor + else: + cursor = self.normal_cursor + + view.get_window(Gtk.TextWindowType.TEXT).set_cursor(cursor) + + def on_view_motion_notify_event(self, view, event): + if event.window == view.get_window(Gtk.TextWindowType.TEXT): + self.update_cursor_style(view, int(event.x), int(event.y)) + + return False + + def on_view_visibility_notify_event(self, view, event): + if event.window == view.get_window(Gtk.TextWindowType.TEXT): + win, x, y, flags = event.window.get_pointer() + self.update_cursor_style(view, x, y) + + return False + + def idle_grab_focus(self): + self.window.get_active_view().grab_focus() + return False + + def get_link_at_location(self, view, x, y): + """ + Get the link under a specified x,y coordinate. If no link exists then + None is returned. + """ + + # get the offset within the buffer from the x,y coordinates + buff_x, buff_y = view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, x, y) + (over_text, iter_at_xy) = view.get_iter_at_location(buff_x, buff_y) + if not over_text: + return None + offset = iter_at_xy.get_offset() + + # find the first link that contains the offset + for lnk in self.links: + if offset >= lnk.start and offset <= lnk.end: + return lnk + + # no link was found at x,y + return None + + def on_view_button_press_event(self, view, event): + if event.button != 1 or event.type != Gdk.EventType.BUTTON_PRESS or \ + event.window != view.get_window(Gtk.TextWindowType.TEXT): + return False + + link = self.get_link_at_location(view, int(event.x), int(event.y)) + if link is None: + return False + + gfile = self.file_lookup.lookup(link.path) + + if gfile: + Gedit.commands_load_location(self.window, gfile, None, link.line_nr, link.col_nr) + GLib.idle_add(self.idle_grab_focus) + +# ex:ts=4:et: diff --git a/plugins/externaltools/tools/outputpanel.ui b/plugins/externaltools/tools/outputpanel.ui new file mode 100644 index 0000000..4c163c2 --- /dev/null +++ b/plugins/externaltools/tools/outputpanel.ui @@ -0,0 +1,49 @@ + + + + + True + False + + + True + True + True + True + + + True + True + False + word + False + False + + + + + + + + + True + False + True + True + end + end + 2 + 2 + Stop Tool + + + + True + False + process-stop-symbolic + + + + + + diff --git a/plugins/externaltools/tools/tools.ui b/plugins/externaltools/tools/tools.ui new file mode 100644 index 0000000..a3f0ab1 --- /dev/null +++ b/plugins/externaltools/tools/tools.ui @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + Always available + always + + + All documents + all + + + All documents except untitled ones + titled + + + Local files only + local + + + Remote files only + remote + + + Untitled documents only + untitled + + + + + + + + + + + + + Nothing + nothing + + + Current document + document + + + Current selection + selection + + + Current selection (default to document) + selection-document + + + Current line + line + + + Current word + word + + + + + + + + + + + + + Nothing + nothing + + + Display in bottom pane + output-panel + + + Create new document + new-document + + + Append to current document + append-document + + + Replace current document + replace-document + + + Replace current selection + replace-selection + + + Insert at cursor position + insert + + + + + + + + + + + + + Nothing + nothing + + + Current document + document + + + All documents + all + + + + + False + Manage External Tools + 800 + 600 + dialog + + + + + + True + Manage External Tools + True + + + + + True + True + True + 275 + True + + + + True + False + vertical + + + True + True + in + + + + True + True + False + True + + + + + + + + True + True + 0 + + + + + True + False + icons + + 1 + + + True + False + Add a new tool + Add Tool + True + list-add-symbolic + + + + False + True + + + + + True + False + Remove selected tool + Remove Tool + True + list-remove-symbolic + + + + False + True + + + + + True + False + Revert tool + Revert Tool + True + edit-undo-symbolic + + + + False + True + + + + + False + True + 1 + + + + + False + False + + + + + True + False + vertical + 6 + + + True + False + + + True + False + + + True + False + 6 + 6 + 6 + 6 + 6 + 6 + + + True + False + 0 + Shortcut _key: + True + accelerator + True + + + 0 + 0 + 1 + 1 + + + + + True + True + True + + + + + + + 1 + 0 + 1 + 1 + + + + + True + False + 0 + _Save: + True + save-files + + + 0 + 1 + 1 + 1 + + + + + True + False + model_save_files + + + + 0 + + + + + 1 + 1 + 1 + 1 + + + + + True + False + 0 + _Input: + True + input + + + 0 + 2 + 1 + 1 + + + + + True + False + model_input + + + + 0 + + + + + 1 + 2 + 1 + 1 + + + + + True + False + 0 + _Output: + True + output + + + 0 + 3 + 1 + 1 + + + + + True + False + model_output + + + + 0 + + + + + 1 + 3 + 1 + 1 + + + + + True + False + 0 + _Applicability: + True + applicability + + + 0 + 4 + 1 + 1 + + + + + True + False + 6 + + + True + False + model_applicability + + + + + 0 + + + + + False + True + 0 + + + + + False + True + True + False + + + + True + False + 0 + All Languages + end + 13 + + + + + True + True + 1 + + + + + 1 + 4 + 1 + 1 + + + + + 0 + 1 + 1 + 1 + + + + + True + True + True + True + in + + + + commands_buffer + True + + + + + 0 + 0 + 1 + 1 + + + + + False + True + 0 + + + + + True + True + 1 + + + + + True + False + + + + + + diff --git a/plugins/externaltools/tools/windowactivatable.py b/plugins/externaltools/tools/windowactivatable.py new file mode 100644 index 0000000..5949598 --- /dev/null +++ b/plugins/externaltools/tools/windowactivatable.py @@ -0,0 +1,141 @@ +# -*- coding: UTF-8 -*- +# Gedit External Tools plugin +# Copyright (C) 2005-2006 Steve Frécinaux +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +__all__ = ('ExternalToolsPlugin', 'OutputPanel', 'Capture', 'UniqueById') + +from gi.repository import GLib, Gio, GObject, Gtk, Gedit +from .library import ToolLibrary +from .outputpanel import OutputPanel +from .capture import Capture +from .functions import * + +try: + import gettext + gettext.bindtextdomain('gedit') + gettext.textdomain('gedit') + _ = gettext.gettext +except: + _ = lambda s: s + +class ToolActions(object): + def __init__(self, library, window, panel): + super(ToolActions, self).__init__() + self._library = library + self._window = window + self._panel = panel + self._action_tools = {} + + self.update() + + def deactivate(self): + self.remove() + + def remove(self): + for name, tool in self._action_tools.items(): + self._window.remove_action(name) + self._action_tools = {} + + def _insert_directory(self, directory): + for tool in sorted(directory.tools, key=lambda x: x.name.lower()): + # FIXME: find a better way to share the action name + action_name = 'external-tool-%X-%X' % (id(tool), id(tool.name)) + self._action_tools[action_name] = tool + + action = Gio.SimpleAction(name=action_name) + action.connect('activate', capture_menu_action, self._window, self._panel, tool) + self._window.add_action(action) + + def update(self): + self.remove() + self._insert_directory(self._library.tree) + self.filter(self._window.get_active_document()) + + def filter_language(self, language, item): + if not item.languages: + return True + + if not language and 'plain' in item.languages: + return True + + if language and (language.get_id() in item.languages): + return True + else: + return False + + def filter(self, document): + if document is None: + titled = False + remote = False + language = None + else: + titled = document.get_file().get_location() is not None + remote = not document.get_file().is_local() + language = document.get_language() + + states = { + 'always': True, + 'all': document is not None, + 'local': titled and not remote, + 'remote': titled and remote, + 'titled': titled, + 'untitled': not titled, + } + + for name, tool in self._action_tools.items(): + action = self._window.lookup_action(name) + if action: + action.set_enabled(states[tool.applicability] and + self.filter_language(language, tool)) + + +class WindowActivatable(GObject.Object, Gedit.WindowActivatable): + __gtype_name__ = "ExternalToolsWindowActivatable" + + window = GObject.Property(type=Gedit.Window) + + def __init__(self): + GObject.Object.__init__(self) + self.actions = None + + def do_activate(self): + self.window.external_tools_window_activatable = self + + self._library = ToolLibrary() + + # Create output console + self._output_buffer = OutputPanel(self.plugin_info.get_data_dir(), self.window) + + self.actions = ToolActions(self._library, self.window, self._output_buffer) + + bottom = self.window.get_bottom_panel() + bottom.add_titled(self._output_buffer.panel, "GeditExternalToolsShellOutput", _("Tool Output")) + + def do_update_state(self): + if self.actions is not None: + self.actions.filter(self.window.get_active_document()) + + def do_deactivate(self): + self.actions.deactivate() + bottom = self.window.get_bottom_panel() + bottom.remove(self._output_buffer.panel) + self.window.external_tools_window_activatable = None + + def update_actions(self): + self.actions.update() + +# ex:ts=4:et: -- cgit v1.2.3