summaryrefslogtreecommitdiffstats
path: root/plugins/externaltools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:32:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:32:59 +0000
commitadb934701975f6b0214475d1a8d0d1ce727b9d4d (patch)
tree5688c745d10b64c8856586864ec416a6bdae881d /plugins/externaltools
parentInitial commit. (diff)
downloadgedit-adb934701975f6b0214475d1a8d0d1ce727b9d4d.tar.xz
gedit-adb934701975f6b0214475d1a8d0d1ce727b9d4d.zip
Adding upstream version 3.38.1.upstream/3.38.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/externaltools')
-rw-r--r--plugins/externaltools/data/build.desktop.in9
-rwxr-xr-xplugins/externaltools/data/build.tool.in15
-rw-r--r--plugins/externaltools/data/meson.build46
-rw-r--r--plugins/externaltools/data/open-terminal-here-osx.desktop.in9
-rwxr-xr-xplugins/externaltools/data/open-terminal-here-osx.tool.in16
-rw-r--r--plugins/externaltools/data/open-terminal-here.desktop.in9
-rwxr-xr-xplugins/externaltools/data/open-terminal-here.tool.in4
-rw-r--r--plugins/externaltools/data/remove-trailing-spaces.desktop.in9
-rwxr-xr-xplugins/externaltools/data/remove-trailing-spaces.tool.in3
-rw-r--r--plugins/externaltools/data/run-command.desktop.in8
-rwxr-xr-xplugins/externaltools/data/run-command.tool.in4
-rw-r--r--plugins/externaltools/data/send-to-fpaste.desktop.in11
-rwxr-xr-xplugins/externaltools/data/send-to-fpaste.tool.in26
-rw-r--r--plugins/externaltools/externaltools.plugin.desktop.in9
-rw-r--r--plugins/externaltools/meson.build35
-rw-r--r--plugins/externaltools/org.gnome.gedit.plugins.externaltools.gschema.xml20
-rwxr-xr-xplugins/externaltools/scripts/gedit-tool-merge.pl78
-rw-r--r--plugins/externaltools/scripts/meson.build13
-rw-r--r--plugins/externaltools/tests/meson.build21
-rw-r--r--plugins/externaltools/tests/testlinkparsing.py203
-rw-r--r--plugins/externaltools/tools/__init__.py26
-rw-r--r--plugins/externaltools/tools/appactivatable.py178
-rw-r--r--plugins/externaltools/tools/capture.py268
-rw-r--r--plugins/externaltools/tools/filelookup.py165
-rw-r--r--plugins/externaltools/tools/functions.py365
-rw-r--r--plugins/externaltools/tools/library.py520
-rw-r--r--plugins/externaltools/tools/linkparsing.py252
-rw-r--r--plugins/externaltools/tools/manager.py878
-rw-r--r--plugins/externaltools/tools/meson.build36
-rw-r--r--plugins/externaltools/tools/outputpanel.py247
-rw-r--r--plugins/externaltools/tools/outputpanel.ui49
-rw-r--r--plugins/externaltools/tools/tools.ui548
-rw-r--r--plugins/externaltools/tools/windowactivatable.py141
33 files changed, 4221 insertions, 0 deletions
diff --git a/plugins/externaltools/data/build.desktop.in b/plugins/externaltools/data/build.desktop.in
new file mode 100644
index 0000000..14dfebd
--- /dev/null
+++ b/plugins/externaltools/data/build.desktop.in
@@ -0,0 +1,9 @@
+[Gedit Tool]
+Name=Build
+Comment=Run “make” in the document directory
+Input=nothing
+Output=output-panel
+Shortcut=<Control>F8
+Applicability=local
+Save-files=all
+Languages=
diff --git a/plugins/externaltools/data/build.tool.in b/plugins/externaltools/data/build.tool.in
new file mode 100755
index 0000000..0b81d5b
--- /dev/null
+++ b/plugins/externaltools/data/build.tool.in
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+EHOME=`echo $HOME | sed "s/#/\#/"`
+DIR=$GEDIT_CURRENT_DOCUMENT_DIR
+while test "$DIR" != "/"; do
+ for m in GNUmakefile makefile Makefile; do
+ if [ -f "${DIR}/${m}" ]; then
+ echo "Using ${m} from ${DIR}" | sed "s#$EHOME#~#" > /dev/stderr
+ make -C "${DIR}"
+ exit
+ fi
+ done
+ DIR=`dirname "${DIR}"`
+done
+echo "No Makefile found!" > /dev/stderr
diff --git a/plugins/externaltools/data/meson.build b/plugins/externaltools/data/meson.build
new file mode 100644
index 0000000..02d5d6c
--- /dev/null
+++ b/plugins/externaltools/data/meson.build
@@ -0,0 +1,46 @@
+externaltools_tools = [
+ 'build',
+ 'remove-trailing-spaces',
+ 'send-to-fpaste',
+]
+
+if host_machine.system() == 'darwin'
+ externaltools_tools += [
+ 'open-terminal-here-osx',
+ ]
+elif host_machine.system() != 'windows'
+ externaltools_tools += [
+ 'open-terminal-here',
+ 'run-command',
+ ]
+endif
+
+foreach tool_name: externaltools_tools
+ dektop_file = custom_target(
+ '@0@.desktop'.format(tool_name),
+ input: '@0@.desktop.in'.format(tool_name),
+ output: '@0@.desktop'.format(tool_name),
+ command: msgfmt_externaltools_cmd,
+ install: false,
+ )
+
+ custom_target(
+ '@0@.tool'.format(tool_name),
+ input: '@0@.tool.in'.format(tool_name),
+ output: '@0@'.format(tool_name),
+ depends: dektop_file,
+ command: [
+ merge_tool_prg,
+ '@INPUT@',
+ dektop_file.full_path(),
+ ],
+ capture: true,
+ install: true,
+ install_dir: join_paths(
+ pkgdatadir,
+ 'plugins',
+ 'externaltools',
+ 'tools',
+ )
+ )
+endforeach
diff --git a/plugins/externaltools/data/open-terminal-here-osx.desktop.in b/plugins/externaltools/data/open-terminal-here-osx.desktop.in
new file mode 100644
index 0000000..846ff9a
--- /dev/null
+++ b/plugins/externaltools/data/open-terminal-here-osx.desktop.in
@@ -0,0 +1,9 @@
+[Gedit Tool]
+Name=Open terminal here
+Comment=Open a terminal in the document location
+Input=nothing
+Output=output-panel
+Applicability=local
+Save-files=nothing
+Languages=
+Shortcut=<Primary><Alt>t
diff --git a/plugins/externaltools/data/open-terminal-here-osx.tool.in b/plugins/externaltools/data/open-terminal-here-osx.tool.in
new file mode 100755
index 0000000..c336006
--- /dev/null
+++ b/plugins/externaltools/data/open-terminal-here-osx.tool.in
@@ -0,0 +1,16 @@
+#!/usr/bin/osascript
+
+set the_path to system attribute "GEDIT_CURRENT_DOCUMENT_DIR"
+set cmd to "cd " & quoted form of the_path
+
+tell application "System Events" to set terminalIsRunning to exists application process "Terminal"
+
+tell application "Terminal"
+ activate
+
+ if terminalIsRunning is true then
+ do script with command cmd
+ else
+ do script with command cmd in window 1
+ end if
+end tell
diff --git a/plugins/externaltools/data/open-terminal-here.desktop.in b/plugins/externaltools/data/open-terminal-here.desktop.in
new file mode 100644
index 0000000..846ff9a
--- /dev/null
+++ b/plugins/externaltools/data/open-terminal-here.desktop.in
@@ -0,0 +1,9 @@
+[Gedit Tool]
+Name=Open terminal here
+Comment=Open a terminal in the document location
+Input=nothing
+Output=output-panel
+Applicability=local
+Save-files=nothing
+Languages=
+Shortcut=<Primary><Alt>t
diff --git a/plugins/externaltools/data/open-terminal-here.tool.in b/plugins/externaltools/data/open-terminal-here.tool.in
new file mode 100755
index 0000000..9365648
--- /dev/null
+++ b/plugins/externaltools/data/open-terminal-here.tool.in
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+#TODO: use "gconftool-2 -g /desktop/gnome/applications/terminal/exec"
+gnome-terminal --working-directory="$GEDIT_CURRENT_DOCUMENT_DIR" &
diff --git a/plugins/externaltools/data/remove-trailing-spaces.desktop.in b/plugins/externaltools/data/remove-trailing-spaces.desktop.in
new file mode 100644
index 0000000..9a34de7
--- /dev/null
+++ b/plugins/externaltools/data/remove-trailing-spaces.desktop.in
@@ -0,0 +1,9 @@
+[Gedit Tool]
+Name=Remove trailing spaces
+Comment=Remove useless trailing spaces in your file
+Input=document
+Output=replace-document
+Shortcut=<Alt>F12
+Applicability=all
+Save-files=nothing
+Languages=
diff --git a/plugins/externaltools/data/remove-trailing-spaces.tool.in b/plugins/externaltools/data/remove-trailing-spaces.tool.in
new file mode 100755
index 0000000..83e4c19
--- /dev/null
+++ b/plugins/externaltools/data/remove-trailing-spaces.tool.in
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+sed 's/[[:blank:]]*$//'
diff --git a/plugins/externaltools/data/run-command.desktop.in b/plugins/externaltools/data/run-command.desktop.in
new file mode 100644
index 0000000..ca7b7da
--- /dev/null
+++ b/plugins/externaltools/data/run-command.desktop.in
@@ -0,0 +1,8 @@
+[Gedit Tool]
+Name=Run command
+Comment=Execute a custom command and put its output in a new document
+Input=nothing
+Output=new-document
+Applicability=all
+Save-files=nothing
+Languages=
diff --git a/plugins/externaltools/data/run-command.tool.in b/plugins/externaltools/data/run-command.tool.in
new file mode 100755
index 0000000..77ad4e6
--- /dev/null
+++ b/plugins/externaltools/data/run-command.tool.in
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+#TODO: use "gconftool-2 -g /desktop/gnome/applications/terminal/exec"
+eval $(zenity --entry --title="Run Command - gedit" --text="Command to run:")
diff --git a/plugins/externaltools/data/send-to-fpaste.desktop.in b/plugins/externaltools/data/send-to-fpaste.desktop.in
new file mode 100644
index 0000000..40282c8
--- /dev/null
+++ b/plugins/externaltools/data/send-to-fpaste.desktop.in
@@ -0,0 +1,11 @@
+[Gedit Tool]
+Name=Send to fpaste
+Comment=Paste selected text or current document to fpaste
+Input=selection-document
+Output=output-panel
+Shortcut=<Shift><Super>p
+Applicability=always
+Save-files=nothing
+Languages=
+
+
diff --git a/plugins/externaltools/data/send-to-fpaste.tool.in b/plugins/externaltools/data/send-to-fpaste.tool.in
new file mode 100755
index 0000000..d392173
--- /dev/null
+++ b/plugins/externaltools/data/send-to-fpaste.tool.in
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+import os, urllib, json, sys, urllib.request
+from gi.repository import Gtk, Gdk
+
+text = sys.stdin.read()
+
+lang = os.getenv('GEDIT_CURRENT_DOCUMENT_LANGUAGE')
+if lang is None:
+ lang = "text"
+
+url_params = urllib.parse.urlencode({'paste_data': text, 'paste_lang': lang, 'mode':'json', 'api_submit':'true'})
+openfpaste = urllib.request.urlopen("http://fpaste.org", bytes(url_params, 'utf-8')).read().decode("utf-8")
+if openfpaste is None:
+ print("Failed to send fpaste request.")
+
+final_data = json.loads(openfpaste)
+
+paste_url = "http://fpaste.org/" + final_data['result']['id']
+
+disp = Gdk.Display.get_default()
+clipboard = Gtk.Clipboard.get_for_display(disp, Gdk.SELECTION_CLIPBOARD)
+clipboard.set_text(paste_url, len(paste_url))
+clipboard.store()
+
+print(paste_url + " has been copied to the clipboard.")
diff --git a/plugins/externaltools/externaltools.plugin.desktop.in b/plugins/externaltools/externaltools.plugin.desktop.in
new file mode 100644
index 0000000..f575818
--- /dev/null
+++ b/plugins/externaltools/externaltools.plugin.desktop.in
@@ -0,0 +1,9 @@
+[Plugin]
+Loader=python3
+Module=externaltools
+IAge=3
+Name=External Tools
+Description=Execute external commands and shell scripts.
+Authors=Steve Frécinaux <steve@istique.net>
+Copyright=Copyright © 2005 Steve Frécinaux
+Website=http://www.gedit.org
diff --git a/plugins/externaltools/meson.build b/plugins/externaltools/meson.build
new file mode 100644
index 0000000..d46d0dd
--- /dev/null
+++ b/plugins/externaltools/meson.build
@@ -0,0 +1,35 @@
+subdir('scripts')
+subdir('tools')
+subdir('data')
+
+externaltools_gschema_file = files('org.gnome.gedit.plugins.externaltools.gschema.xml')
+install_data(
+ externaltools_gschema_file,
+ install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'glib-2.0/schemas')
+)
+
+if xmllint.found()
+ test(
+ 'validate-externaltools-gschema',
+ xmllint,
+ args: [
+ '--noout',
+ '--dtdvalid', gschema_dtd,
+ externaltools_gschema_file,
+ ]
+ )
+endif
+
+custom_target(
+ 'externaltools.plugin',
+ input: 'externaltools.plugin.desktop.in',
+ output: 'externaltools.plugin',
+ command: msgfmt_plugin_cmd,
+ install: true,
+ install_dir: join_paths(
+ pkglibdir,
+ 'plugins',
+ )
+)
+
+subdir('tests')
diff --git a/plugins/externaltools/org.gnome.gedit.plugins.externaltools.gschema.xml b/plugins/externaltools/org.gnome.gedit.plugins.externaltools.gschema.xml
new file mode 100644
index 0000000..d760de2
--- /dev/null
+++ b/plugins/externaltools/org.gnome.gedit.plugins.externaltools.gschema.xml
@@ -0,0 +1,20 @@
+<schemalist gettext-domain="gedit">
+ <schema id="org.gnome.gedit.plugins.externaltools" path="/org/gnome/gedit/plugins/externaltools/">
+ <key name="use-system-font" type="b">
+ <default>true</default>
+ <summary>Whether to use the system font</summary>
+ <description>
+ If true, the external tools will use the desktop-global standard
+ font if it’s monospace (and the most similar font it can
+ come up with otherwise).
+ </description>
+ </key>
+ <key name="font" type="s">
+ <default>'Monospace 10'</default>
+ <summary>Font</summary>
+ <description>
+ A Pango font name. Examples are “Sans 12” or “Monospace Bold 14”.
+ </description>
+ </key>
+ </schema>
+</schemalist>
diff --git a/plugins/externaltools/scripts/gedit-tool-merge.pl b/plugins/externaltools/scripts/gedit-tool-merge.pl
new file mode 100755
index 0000000..b7cbd77
--- /dev/null
+++ b/plugins/externaltools/scripts/gedit-tool-merge.pl
@@ -0,0 +1,78 @@
+#!/usr/bin/env perl
+
+# gedit-tool-merge.pl
+# This file is part of gedit
+#
+# Copyright (C) 2006 - Steve Frécinaux <code@istique.net>
+#
+# gedit 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.
+#
+# gedit 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 gedit; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301 USA
+
+# This script merges a script file with a desktop file containing
+# metadata about the external tool. This is required in order to
+# have translatable tools (bug #342042) since intltool can't extract
+# string directly from tool files (a tool file being the combination
+# of a script file and a metadata section).
+#
+# The desktop file is embedded in a comment of the script file, under
+# the assumption that any scripting language supports # as a comment
+# mark (this is likely to be true since the shebang uses #!). The
+# section is placed at the top of the tool file, after the shebang and
+# modelines if present.
+
+use strict;
+use warnings;
+use Getopt::Long;
+
+sub usage {
+ print <<EOF;
+Usage: ${0} [OPTION]... [SCRIPT] [DESKTOP]
+Merges a script file with a desktop file, embedding it in a comment.
+
+ -o, --output=FILE Specify the output file name. [default: stdout]
+EOF
+ exit;
+}
+
+my $output = "";
+my $help;
+
+GetOptions ("help|h" => \$help, "output|o=s" => \$output) or &usage;
+usage if $help or @ARGV lt 2;
+
+open INFILE, "<", $ARGV[0];
+open DFILE, "<", $ARGV[1];
+open STDOUT, ">", $output if $output;
+
+# Put shebang and various modelines at the top of the generated file.
+$_ = <INFILE>;
+print and $_ = <INFILE> if /^#!/;
+print and $_ = <INFILE> if /-\*-/;
+print and $_ = <INFILE> if /(ex|vi|vim):/;
+
+# Put a blank line before the info block if there is one in INFILE.
+print and $_ = <INFILE> if /^\s*$/;
+seek INFILE, -length, 1;
+
+# Embed the desktop file...
+print "# $_" while <DFILE>;
+print "\n";
+
+# ...and write the remaining part of the script.
+print while <INFILE>;
+
+close INFILE;
+close DFILE;
+close STDOUT;
diff --git a/plugins/externaltools/scripts/meson.build b/plugins/externaltools/scripts/meson.build
new file mode 100644
index 0000000..c5f50e9
--- /dev/null
+++ b/plugins/externaltools/scripts/meson.build
@@ -0,0 +1,13 @@
+msgfmt_externaltools_cmd = [
+ find_program('msgfmt'),
+ '--desktop',
+ '--keyword=Name',
+ '--keyword=Comment',
+ '--template=@INPUT@',
+ '-d', join_paths(srcdir, 'po'),
+ '--output=@OUTPUT@'
+]
+
+merge_tool_prg = find_program(
+ files('gedit-tool-merge.pl'),
+)
diff --git a/plugins/externaltools/tests/meson.build b/plugins/externaltools/tests/meson.build
new file mode 100644
index 0000000..6cee9fe
--- /dev/null
+++ b/plugins/externaltools/tests/meson.build
@@ -0,0 +1,21 @@
+externaltools_tests = {
+ 'LinkParser': files('testlinkparsing.py'),
+}
+
+externaltools_srcdir = join_paths(
+ srcdir,
+ 'plugins',
+ 'externaltools',
+ 'tools',
+)
+
+foreach test_name, test_script : externaltools_tests
+ test(
+ 'test-externaltools-@0@'.format(test_name),
+ python3,
+ args: [test_script],
+ env: [
+ 'PYTHONPATH=@0@'.format(externaltools_srcdir),
+ ]
+ )
+endforeach
diff --git a/plugins/externaltools/tests/testlinkparsing.py b/plugins/externaltools/tests/testlinkparsing.py
new file mode 100644
index 0000000..3b8a78e
--- /dev/null
+++ b/plugins/externaltools/tests/testlinkparsing.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2010 Per Arneng <per.arneng@anyplanet.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 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 unittest
+from linkparsing import LinkParser
+
+
+class TestLinkParser(unittest.TestCase):
+
+ def setUp(self):
+ self.p = LinkParser()
+
+ def assert_link_count(self, links, expected_count):
+ self.assertEqual(len(links), expected_count, 'incorrect nr of links')
+
+ def assert_link(self, actual, path, line_nr, col_nr=0):
+ self.assertEqual(actual.path, path, "incorrect path")
+ self.assertEqual(actual.line_nr, line_nr, "incorrect line nr")
+ self.assertEqual(actual.col_nr, col_nr, "incorrect col nr")
+
+ def assert_link_text(self, text, link, link_text):
+ self.assertEqual(text[link.start:link.end], link_text,
+ "the expected link text does not match the text within the string")
+
+ def test_parse_gcc_simple_test_with_real_output(self):
+ gcc_output = """
+test.c: In function 'f':
+test.c:5:6: warning: passing argument 1 of 'f' makes integer from pointer without a cast
+test.c:3:7: note: expected 'int' but argument is of type 'char *'
+test.c: In function 'main':
+test.c:11:10: warning: initialization makes pointer from integer without a cast
+test.c:12:11: warning: initialization makes integer from pointer without a cast
+test.c:13:12: error: too few arguments to function 'f'
+test.c:14:13: error: expected ';' before 'return'
+"""
+ links = self.p.parse(gcc_output)
+ self.assert_link_count(links, 6)
+ lnk = links[2]
+ self.assert_link(lnk, "test.c", 11, 10)
+ self.assert_link_text(gcc_output, lnk, "test.c:11:10")
+
+ def test_parse_gcc_one_line(self):
+ line = "/tmp/myfile.c:1212:12: error: ..."
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "/tmp/myfile.c", 1212, 12)
+ self.assert_link_text(line, lnk, "/tmp/myfile.c:1212:12")
+
+ def test_parse_gcc_empty_string(self):
+ links = self.p.parse("")
+ self.assert_link_count(links, 0)
+
+ def test_parse_gcc_no_files_in_text(self):
+ links = self.p.parse("no file links in this string")
+ self.assert_link_count(links, 0)
+
+ def test_parse_gcc_none_as_argument(self):
+ self.assertRaises(ValueError, self.p.parse, None)
+
+ def test_parse_grep_one_line(self):
+ line = "libnautilus-private/nautilus-canvas-container.h:45:#define NAUTILUS_CANVAS_ICON_DATA(pointer)"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "libnautilus-private/nautilus-canvas-container.h", 45)
+ self.assert_link_text(line, lnk, "libnautilus-private/nautilus-canvas-container.h:45")
+
+ def test_parse_python_simple_test_with_real_output(self):
+ output = """
+Traceback (most recent call last):
+ File "test.py", line 10, in <module>
+ err()
+ File "test.py", line 7, in err
+ real_err()
+ File "test.py", line 4, in real_err
+ int('xxx')
+ValueError: invalid literal for int() with base 10: 'xxx'
+"""
+ links = self.p.parse(output)
+ self.assert_link_count(links, 3)
+ lnk = links[2]
+ self.assert_link(lnk, "test.py", 4)
+ self.assert_link_text(output, lnk, '"test.py", line 4')
+
+ def test_parse_python_one_line(self):
+ line = " File \"test.py\", line 1\n def a()"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "test.py", 1)
+ self.assert_link_text(line, lnk, '"test.py", line 1')
+
+ def test_parse_bash_one_line(self):
+ line = "test.sh: line 5: gerp: command not found"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "test.sh", 5)
+ self.assert_link_text(line, lnk, 'test.sh: line 5')
+
+ def test_parse_javac_one_line(self):
+ line = "/tmp/Test.java:10: incompatible types"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "/tmp/Test.java", 10)
+ self.assert_link_text(line, lnk, '/tmp/Test.java:10')
+
+ def test_parse_valac_simple_test_with_real_output(self):
+ output = """
+Test.vala:14.13-14.21: error: Assignment: Cannot convert from `string' to `int'
+ int a = "xxx";
+ ^^^^^^^^^
+"""
+ links = self.p.parse(output)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "Test.vala", 14)
+ self.assert_link_text(output, lnk, 'Test.vala:14.13-14.21')
+
+ def test_parse_ruby_simple_test_with_real_output(self):
+ output = """
+test.rb:5: undefined method `fake_method' for main:Object (NoMethodError)
+ from test.rb:3:in `each'
+ from test.rb:3
+"""
+ links = self.p.parse(output)
+ self.assert_link_count(links, 3)
+ lnk = links[0]
+ self.assert_link(lnk, "test.rb", 5)
+ self.assert_link_text(output, lnk, 'test.rb:5')
+ lnk = links[1]
+ self.assert_link(lnk, "test.rb", 3)
+ self.assert_link_text(output, lnk, 'test.rb:3')
+
+ def test_parse_scalac_one_line(self):
+ line = "Test.scala:7: error: not found: value fakeMethod"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "Test.scala", 7)
+ self.assert_link_text(line, lnk, 'Test.scala:7')
+
+ def test_parse_sbt_one_line(self):
+ line = "[error] /home/hank/foo/Test.scala:7: not found: value fakeMethod"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "/home/hank/foo/Test.scala", 7)
+ self.assert_link_text(line, lnk, '/home/hank/foo/Test.scala:7')
+
+ def test_parse_go_6g_one_line(self):
+ line = "test.go:9: undefined: FakeMethod"
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "test.go", 9)
+ self.assert_link_text(line, lnk, 'test.go:9')
+
+ def test_parse_perl_one_line(self):
+ line = 'syntax error at test.pl line 889, near "$fake_var'
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "test.pl", 889)
+ self.assert_link_text(line, lnk, 'test.pl line 889')
+
+ def test_parse_mcs_one_line(self):
+ line = 'Test.cs(12,7): error CS0103: The name `fakeMethod'
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "Test.cs", 12)
+ self.assert_link_text(line, lnk, 'Test.cs(12,7)')
+
+ def test_parse_pas_one_line(self):
+ line = 'hello.pas(11,1) Fatal: Syntax error, ":" expected but "BEGIN"'
+ links = self.p.parse(line)
+ self.assert_link_count(links, 1)
+ lnk = links[0]
+ self.assert_link(lnk, "hello.pas", 11)
+ self.assert_link_text(line, lnk, 'hello.pas(11,1)')
+
+if __name__ == '__main__':
+ unittest.main()
+
+# ex:ts=4:et:
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 <icq@gnome.org>
+#
+# 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 <steve@istique.net>
+#
+# 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 <steve@istique.net>
+#
+# 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 <per.arneng@anyplanet.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 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 <steve@istique.net>
+#
+# 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 <code@istique.net>
+#
+# 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 <per.arneng@anyplanet.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 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<lnk>
+ (?P<pth> [^ \:\n]* )
+ \:
+ (?P<ln> \d+)
+ \:?
+ (?P<col> \d+)?
+)
+\:"""
+
+# python ' File "test.py", line 13'
+REGEXP_PYTHON = r"""
+^\s\sFile\s
+(?P<lnk>
+ \"
+ (?P<pth> [^\"]+ )
+ \",\sline\s
+ (?P<ln> \d+ )
+)"""
+
+# python 'test.sh: line 5:'
+REGEXP_BASH = r"""
+^(?P<lnk>
+ (?P<pth> .* )
+ \:\sline\s
+ (?P<ln> \d+ )
+)\:"""
+
+# valac 'Test.vala:13.1-13.3: ...'
+REGEXP_VALAC = r"""
+^(?P<lnk>
+ (?P<pth>
+ .*vala
+ )
+ \:
+ (?P<ln>
+ \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<lnk>
+ (?P<pth>
+ .*
+ )
+ \:
+ (?P<ln>
+ \d+
+ )
+ )"""
+
+# perl 'syntax error at test.pl line 88, near "$fake_var'
+REGEXP_PERL = r"""
+\sat\s
+(?P<lnk>
+ (?P<pth> .* )
+ \sline\s
+ (?P<ln> \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<lnk>
+ (?P<pth> \S+ )
+ \(
+ (?P<ln> \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 <steve@istique.net>
+#
+# 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 (<b>%s</b>)' % (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 <steve@istique.net>
+# Copyright (C) 2010 Per Arneng <per.arneng@anyplanet.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.6 -->
+ <object class="GtkOverlay" id="output-panel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkTextView" id="view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="cursor_visible">False</property>
+ <property name="accepts_tab">False</property>
+ <signal name="visibility-notify-event" handler="on_view_visibility_notify_event" swapped="no"/>
+ <signal name="motion-notify-event" handler="on_view_motion_notify_event" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkButton" id="stop">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="valign">end</property>
+ <property name="halign">end</property>
+ <property name="margin_bottom">2</property>
+ <property name="margin_end">2</property>
+ <property name="tooltip_text" translatable="yes">Stop Tool</property>
+ <signal name="clicked" handler="on_stop_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">process-stop-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.0 -->
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <object class="GeditDocument" id="commands_buffer"/>
+ <object class="GtkListStore" id="model_applicability">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0" translatable="yes">Always available</col>
+ <col id="1">always</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">All documents</col>
+ <col id="1">all</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">All documents except untitled ones</col>
+ <col id="1">titled</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Local files only</col>
+ <col id="1">local</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Remote files only</col>
+ <col id="1">remote</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Untitled documents only</col>
+ <col id="1">untitled</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkListStore" id="model_input">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0" translatable="yes">Nothing</col>
+ <col id="1">nothing</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current document</col>
+ <col id="1">document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current selection</col>
+ <col id="1">selection</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current selection (default to document)</col>
+ <col id="1">selection-document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current line</col>
+ <col id="1">line</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current word</col>
+ <col id="1">word</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkListStore" id="model_output">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0" translatable="yes">Nothing</col>
+ <col id="1">nothing</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Display in bottom pane</col>
+ <col id="1">output-panel</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Create new document</col>
+ <col id="1">new-document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Append to current document</col>
+ <col id="1">append-document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Replace current document</col>
+ <col id="1">replace-document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Replace current selection</col>
+ <col id="1">replace-selection</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Insert at cursor position</col>
+ <col id="1">insert</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkListStore" id="model_save_files">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0" translatable="yes">Nothing</col>
+ <col id="1">nothing</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">Current document</col>
+ <col id="1">document</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">All documents</col>
+ <col id="1">all</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkWindow" id="tool-manager-dialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Manage External Tools</property>
+ <property name="default_width">800</property>
+ <property name="default_height">600</property>
+ <property name="type_hint">dialog</property>
+ <signal name="configure-event" handler="on_tool_manager_dialog_configure_event" swapped="no"/>
+ <signal name="delete-event" handler="on_tool_manager_dialog_delete_event" swapped="no"/>
+ <signal name="focus-out-event" handler="on_tool_manager_dialog_focus_out" swapped="no"/>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Manage External Tools</property>
+ <property name="show_close_button">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPaned" id="paned">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="position">275</property>
+ <property name="position_set">True</property>
+ <style>
+ <class name="gedit-tool-manager-paned"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="vbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <style>
+ <class name="gedit-tool-manager-treeview"/>
+ </style>
+ <child>
+ <object class="GtkTreeView" id="view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="reorderable">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="treeview-selection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolbar" id="toolbar1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="toolbar_style">icons</property>
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <property name="icon_size">1</property>
+ <child>
+ <object class="GtkToolButton" id="add-tool-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Add a new tool</property>
+ <property name="label" translatable="yes">Add Tool</property>
+ <property name="use_underline">True</property>
+ <property name="icon_name">list-add-symbolic</property>
+ <signal name="clicked" handler="on_add_tool_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="remove-tool-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Remove selected tool</property>
+ <property name="label" translatable="yes">Remove Tool</property>
+ <property name="use_underline">True</property>
+ <property name="icon_name">list-remove-symbolic</property>
+ <signal name="clicked" handler="on_remove_tool_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="revert-tool-button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Revert tool</property>
+ <property name="label" translatable="yes">Revert Tool</property>
+ <property name="use_underline">True</property>
+ <property name="icon_name">edit-undo-symbolic</property>
+ <signal name="clicked" handler="on_remove_tool_button_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="hbox7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid" id="tool-grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Shortcut _key:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">accelerator</property>
+ <property name="hexpand">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="accelerator">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <signal name="backspace" handler="on_accelerator_backspace" swapped="no"/>
+ <signal name="focus-in-event" handler="on_accelerator_focus_in" swapped="no"/>
+ <signal name="focus-out-event" handler="on_accelerator_focus_out" swapped="no"/>
+ <signal name="key-press-event" handler="on_accelerator_key_press" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Save:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">save-files</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="save-files">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">model_save_files</property>
+ <child>
+ <object class="GtkCellRendererText" id="renderer1"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Input:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">input</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="input">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">model_input</property>
+ <child>
+ <object class="GtkCellRendererText" id="input_renderer"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Output:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">output</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="output">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">model_output</property>
+ <child>
+ <object class="GtkCellRendererText" id="output_renderer"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label23">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Applicability:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">applicability</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkComboBox" id="applicability">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">model_applicability</property>
+ <signal name="changed" handler="on_applicability_changed" swapped="no"/>
+ <child>
+ <object class="GtkCellRendererText" id="applicability_renderer"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="languages_button">
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <signal name="clicked" handler="on_languages_button_clicked" swapped="no"/>
+ <child>
+ <object class="GtkLabel" id="languages_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">All Languages</property>
+ <property name="ellipsize">end</property>
+ <property name="width_chars">13</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="shadow_type">in</property>
+ <style>
+ <class name="gedit-tool-manager-view"/>
+ </style>
+ <child>
+ <object class="GeditView" id="commands">
+ <property name="buffer">commands_buffer</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
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 <steve@istique.net>
+#
+# 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: