Skip to content
Snippets Groups Projects
Commit ae4d1b59 authored by Florian Münchbach's avatar Florian Münchbach
Browse files

[syntax_highlight] Added source code syntax highlighting plugin.

parent d6b48053
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
# Syntax Highlighting Plugin for Gajim
[Gajim](https://gajim.org/) Plugin that highlights source code blocks in chatbox.
## Installation
The recommended way of installing this plugin is to use
[Gajim's Plugin Installer](https://dev.gajim.org/gajim/gajim-plugins/#how-to-install).
### Manual Installation
To install the plugin manually clone it into Gajims plugins folder (should be
`~/.local/share/gajim/plugins/` on Linux or
`C:\Users\USERNAME\AppData\Roaming\Gajim\Plugins` on Windows):
```
cd ~/.local/share/gajim/plugins/
git clone https://github.com/FlorianMuenchbach/gajim-syntax-highlight
```
Then restart Gajim and enable the plugin in the plugins menu.
## Usage
Source code between `@@` tags will be highlighted in the chatbox.
You can test it by copying and sending the following text to one of your
contacts:
```
@@def test():
print("Hello, world!")
@@
```
(**Node:** your contact will not receive highlighted text unless she is also
using the plugin.)
If you want to send code written in a programming language other than the
default, you can specify the language between the first `@@` and one additional
`@` tag:
```
@@bash@
echo "Hello, world"
@@
```
## Debug
The plugin adds its own logger. It can be used to set a specific debug level
for this plugin and/or filter log messages.
Run
```
gajim --loglevel gajim.plugin_system.syntax_highlight=DEBUG
```
in a terminal to display the debug messages.
## Known Issues / ToDo
* Gajim crashes when correcting a message containing highlighted code.
## Credits
Since I had no experience in writing Plugins for Gajim, I used the
[Latex Plugin](https://trac-plugins.gajim.org/wiki/LatexPlugin)
written by Yves Fischer and Yann Leboulanger as an example and copied a big
portion of initial code. Therefore, credits go to the authors of the Latex
Plugin for providing an example.
The syntax highlighting itself is done by [pygments](http://pygments.org/).
from syntax_highlight import SyntaxHighlighterPlugin
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="2.16"/>
<object class="GtkListStore" id="line_break_selection">
<columns>
<!-- column-name Text -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Never</col>
</row>
<row>
<col id="0" translatable="yes">Always</col>
</row>
<row>
<col id="0" translatable="yes">Only around multi-line code blocks</col>
</row>
</data>
</object>
<object class="GtkTextBuffer" id="textbuffer1">
<property name="text">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property>
</object>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">9</property>
<property name="spacing">3</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkHBox" id="hbox111">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">3</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Default Language:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="default_lexer_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="lexer_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox11">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">3</property>
<property name="spacing">18</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Add Line breaks:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="line_break_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">line_break_selection</property>
<signal name="changed" handler="line_break_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext2"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
import gtk
import pango
from pygments.formatter import Formatter
class GTKFormatter(Formatter):
name = 'GTK Formatter'
aliases = ['textbuffer', 'gtk']
filenames = []
def __init__(self, **options):
super(GTKFormatter, self).__init__(**options)
#Formatter.__init__(self, **options)
self.tags = {}
self.last_mark = None
self.mark = options.get('start_mark', None)
def insert(self, tb, pos, text):
tb.insert(pos, text)
return tb.get_end_iter()
def get_tag(self, ttype, tb):
tag = None
if ttype in self.tags:
tag = self.tags[ttype]
else:
style = self.style.style_for_token(ttype)
tag = gtk.TextTag()
if 'bgcolor' in style and not style['bgcolor'] is None:
tag.set_property('background', '#%s' % style['bgcolor'])
if 'bold' in style and style['bold']:
tag.set_property('weight', pango.WEIGHT_BOLD)
if 'border' in style and not style['border'] is None:
#TODO
pass
if 'color' in style and not style['color'] is None:
tag.set_property('foreground', '#%s' % style['color'])
if 'italic' in style and style['italic']:
tag.set_property('style', pango.STYLE_ITALIC)
if 'mono' in style and not style['mono'] is None:
tag.set_property('family', 'Monospace')
tag.set_property('family-set', True)
if 'roman' in style and not style['roman'] is None:
#TODO
pass
if 'sans' in style and not style['sans'] is None:
tag.set_property('family', 'Sans')
tag.set_property('family-set', True)
if 'underline' in style and style['underline']:
tag.set_property('underline', 'single')
self.tags[ttype] = tag
tb.get_tag_table().add(tag)
return tag
def set_insert_pos_mark(self, mark):
self.mark = mark
def format(self, tokensource, tb=gtk.TextBuffer()):
if not isinstance(tb, gtk.TextBuffer):
raise TypeError("This Formatter expects a gtk.TextBuffer object as"\
"'output' file argument.")
ltype = None
insert_at_iter = tb.get_iter_at_mark(self.mark) if not self.mark is None \
else tb.get_end_iter()
lstart_iter = tb.create_mark(None, insert_at_iter, True)
lend_iter = tb.create_mark(None, insert_at_iter, False)
for ttype, value in tokensource:
if ttype == ltype:
eiter = self.insert(tb, tb.get_iter_at_mark(lend_iter), value)
#tb.move_mark(lend_iter, eiter)
else:
# set last buffer section properties
if not ltype is None:
# set properties
tag = self.get_tag(ltype, tb)
if not tag is None:
tb.apply_tag(
tag,
tb.get_iter_at_mark(lstart_iter),
tb.get_iter_at_mark(lend_iter))
tb.move_mark(lstart_iter, tb.get_iter_at_mark(lend_iter))
eiter = self.insert(tb, tb.get_iter_at_mark(lend_iter), value)
#tb.move_mark(lend_iter, eiter)
ltype = ttype
self.last_mark = lend_iter
def get_last_mark(self):
return self.last_mark
[info]
name: Source Code Syntax highlight
short_name: syntax_highlight
version: 0.0.2
description: Source Code Syntax Highlighting in the chatbox.<br/><br /><b>Note:</b> You need to have pygments installed. Please refer to the plugin's Wiki page for more information.
authors = Florian Muenchbach
homepage = https://dev.gajim.org/gajim/gajim-plugins/wikis/syntaxhighlightplugin
min_gajim_version: 0.16.4
max_gajim_version: 0.16.9
syntax_highlight/syntax_highlight.png

465 B

import logging
import gtk
from plugins.helpers import log_calls, log
from plugins import GajimPlugin
from plugins.gui import GajimPluginConfigDialog
PYGMENTS_MISSING = 'You are missing Python-Pygments.'
ERROR_MSG = ''
log = logging.getLogger('gajim.plugin_system.syntax_highlight')
DEFAULT_LEXER = "python"
DEFAULT_LINE_BREAK = 2 # => Only on multi-line code blocks
try:
import pygments
from pygments.lexers import get_lexer_by_name, get_all_lexers
from .gtkformatter import GTKFormatter
except Exception as e:
log.error("Import Error: %s.", e)
ERROR_MSG = PYGMENTS_MISSING
class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog):
@log_calls('SyntaxHighlighterPluginConfiguration')
def init(self):
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
'config_dialog.ui')
self.xml = gtk.Builder()
self.xml.set_translation_domain('gajim_plugins')
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['vbox1', 'line_break_selection'])
hbox = self.xml.get_object('vbox1')
self.child.pack_start(hbox)
self.result_label = self.xml.get_object('result_label')
self.liststore = gtk.ListStore(str)
self.default_lexer_combobox = self.xml.get_object('default_lexer_combobox')
self.default_lexer_combobox.set_property("model", self.liststore)
self.line_break_combobox = self.xml.get_object('line_break_combobox')
self.xml.connect_signals(self)
self.default_lexer_id = 0
def set_lexer_list(self, lexers):
self.lexers = lexers
default_lexer = self.plugin.config['default_lexer']
for i, lexer in enumerate(self.lexers):
self.liststore.append([lexer[0]])
if lexer[1] == default_lexer:
self.default_lexer_id = i
def lexer_changed(self, widget):
self.default_lexer_id = self.default_lexer_combobox.get_active()
self.plugin.config['default_lexer'] = self.lexers[self.default_lexer_id][1]
def line_break_changed(self, widget):
self.plugin.config['line_break'] = self.line_break_combobox.get_active()
def on_run(self):
self.default_lexer_combobox.set_active(self.default_lexer_id)
self.line_break_combobox.set_active(self.plugin.config['line_break'])
class SyntaxHighlighterPlugin(GajimPlugin):
def on_change(self, tb):
"""
called when conversation text widget changes
"""
end_iter = tb.get_end_iter()
eol_tag = tb.get_tag_table().lookup('eol')
def split_list(list_):
newlist = []
for i in range(0, len(list_)-1, 2):
newlist.append( [ list_[i], list_[i+1], ] )
return newlist
def get_lexer(language):
lexer = None
try:
lexer = get_lexer_by_name(language)
except:
pass
return lexer
def get_lexer_with_fallback(language, default_lexer):
lexer = get_lexer(language)
if lexer is None:
log.info("Falling back to default lexer for %s.",
str(self.config['default_lexer']))
lexer = get_lexer_by_name(default_lexer)
return lexer
def insert_formatted_code(tb, language, code, mark=None, line_break=False):
lexer = None
if language is None:
log.info("No Language specified. Falling back to default lexer: %s.",
str(self.config['default_lexer']))
lexer = get_lexer(self.config['default_lexer'])
else:
log.debug("Using lexer for %s.", str(language))
lexer = get_lexer_with_fallback(language, self.config['default_lexer'])
if lexer is None:
it = tb.get_iter_at_mark(mark)
tb.insert(it, '\n')
else:
tokens = pygments.lex(code, lexer)
if line_break:
log.debug("Inserting newline before code.")
it = tb.get_iter_at_mark(mark)
tb.insert(it, '\n')
it.forward_char()
tb.move_mark(mark, it)
formatter = GTKFormatter(start_mark=mark)
pygments.format(tokens, formatter, tb)
endmark = formatter.get_last_mark()
if line_break and not endmark is None:
it = tb.get_iter_at_mark(endmark)
tb.insert(it, '\n')
log.debug("Inserting newline after code.")
return tb
def detect_language(tb, start_mark):
language = None
new_start = None
if not start_mark is None:
next_chars = None
lang_iter = tb.get_iter_at_mark(start_mark)
first_word_end = lang_iter.copy()
first_word_end.forward_word_end()
first_word_last_char = first_word_end.get_char()
next_chars_iter = first_word_end.copy()
next_chars_iter.forward_chars(2)
next_chars = first_word_end.get_text(next_chars_iter)
log.debug("first_word_last_char: %s.", first_word_last_char)
if first_word_last_char == "@" and next_chars != "@@":
language = lang_iter.get_text(first_word_end)
log.debug("language: >>%s<<", language)
first_word_end.forward_char()
new_start = tb.create_mark(None, first_word_end, True)
return (language, new_start)
def to_iter(tb, mark):
return tb.get_iter_at_mark(mark)
def check_line_break(tb, start, end):
line_break = False
if self.config['line_break'] == 1:
line_break = True
elif self.config['line_break'] == 2:
# hackish way to check if this code block contains multiple
# lines.
# TODO find better method....
multiline_test = to_iter(tb, start).copy()
multiline_test.forward_line()
line_break = multiline_test.in_range(
to_iter(tb, start), to_iter(tb, end))
return line_break
def replace_code_block(tb, s_tag, s_code, e_tag, e_code):
iter_range_full = (tb.get_iter_at_mark(s_tag),
tb.get_iter_at_mark(e_tag))
text_full = iter_range_full[0].get_text(iter_range_full[1])
log.debug("full text to remove: %s.", text_full)
tb.begin_user_action()
language, code_start = detect_language(tb, s_code)
code_start = s_code if code_start is None else code_start
line_break = check_line_break(tb, code_start, e_code)
code = to_iter(tb, code_start).get_text(to_iter(tb, e_code))
log.debug("full text to encode: %s.", code)
# Delete code between and including tags
tb.delete(to_iter(tb, s_tag), to_iter(tb, e_tag))
insert_formatted_code(tb, language, code, mark=s_tag,
line_break=line_break)
tb.end_user_action()
def detect_tags(tb, start_it=None, end_it=None):
if not end_it:
end_it = tb.get_end_iter()
if not start_it:
eol_tag = tb.get_tag_table().lookup('eol')
start_it = end_it.copy()
start_it.backward_to_tag_toggle(eol_tag)
points = []
tuple_found = start_it.forward_search('@@',
gtk.TEXT_SEARCH_TEXT_ONLY)
while tuple_found != None:
points.append((tb.create_mark(None, tuple_found[0], True),
tb.create_mark(None, tuple_found[1], True)))
tuple_found = tuple_found[1].forward_search('@@',
gtk.TEXT_SEARCH_TEXT_ONLY)
for (s_tag, s_code), (e_code, e_tag) in split_list(points):
replace_code_block(tb, s_tag, s_code, e_tag, e_code)
end_iter = tb.get_end_iter()
eol_tag = tb.get_tag_table().lookup('eol')
it = end_iter.copy()
it.backward_to_tag_toggle(eol_tag)
it1 = it.copy()
it1.backward_char()
it1.backward_to_tag_toggle(eol_tag)
detect_tags(tb, it1, it)
@log_calls('SyntaxHighlighterPlugin')
def connect_with_chat_control(self, control):
control_data = {}
tv = control.conv_textview.tv
control_data['connection'] = tv.get_buffer().connect('changed', self.on_change)
log.debug("connection: %s.", str(control_data['connection']))
control.syntax_highlighter_plugin_data = control_data
@log_calls('SyntaxHighlighterPlugin')
def disconnect_from_chat_control(self, control):
control_data = control.syntax_highlighter_plugin_data
tv = control.conv_textview.tv
log.debug("disconnected: %s.", str(control_data['connection']))
tv.get_buffer().disconnect(control_data['connection'])
def create_lexer_list(self):
self.lexers = []
# Iteration over get_all_lexers() seems to be broken somehow. Workarround
all_lexers = get_all_lexers()
lexer = all_lexers.next()
while not lexer is None:
# We don't want to add lexers that we cant identify by name later
if not lexer[1] is None and len(lexer[1]) > 0:
self.lexers.append((lexer[0], lexer[1][0]))
try:
lexer = all_lexers.next()
except:
lexer = None
self.lexers.sort()
return self.lexers
@log_calls('SyntaxHighlighterPlugin')
def init(self):
if ERROR_MSG:
self.activatable = False
self.available_text = ERROR_MSG
self.config_dialog = None
return
self.config_dialog = SyntaxHighlighterPluginConfiguration(self)
self.config_default_values = {
'default_lexer': (DEFAULT_LEXER, "Default Lexer"),
'line_break': (DEFAULT_LINE_BREAK, "Add line break")
}
self.config_dialog.set_lexer_list(self.create_lexer_list())
self.gui_extension_points = {
'chat_control_base': (
self.connect_with_chat_control,
self.disconnect_from_chat_control
),
}
self.timeout_id = None
self.last_eol_offset = -1
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment