diff options
author | Daniel Brötzmann <mailtrash@posteo.de> | 2020-04-16 19:13:30 +0300 |
---|---|---|
committer | lovetox <philipp@hoerist.com> | 2020-06-29 22:54:50 +0300 |
commit | 83f85dd3e6cf06db2ee4e3a3c1f30949e27f8ffb (patch) | |
tree | 0743c155684661489f5dcc3473cc20579224f42c | |
parent | ae8ce09a1551e767e931d2a53334f22e91a65bed (diff) |
[syntax_highlight] Simplify plugin code
Fix deprecations warnings in config
-rw-r--r-- | syntax_highlight/README.md | 123 | ||||
-rw-r--r-- | syntax_highlight/chat_syntax_highlighter.py | 108 | ||||
-rw-r--r-- | syntax_highlight/config_dialog.py | 206 | ||||
-rw-r--r-- | syntax_highlight/config_dialog.ui | 7 | ||||
-rw-r--r-- | syntax_highlight/gtkformatter.py | 2 | ||||
-rw-r--r-- | syntax_highlight/highlighter_config.py | 97 | ||||
-rw-r--r-- | syntax_highlight/plugin_config.py | 168 | ||||
-rw-r--r-- | syntax_highlight/plugin_config_dialog.py | 151 | ||||
-rw-r--r-- | syntax_highlight/syntax_highlight.py | 150 |
9 files changed, 429 insertions, 583 deletions
diff --git a/syntax_highlight/README.md b/syntax_highlight/README.md deleted file mode 100644 index 29f16cf..0000000 --- a/syntax_highlight/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Syntax Highlighting Plugin for Gajim - -[Gajim](https://gajim.org) Plugin that highlights source code blocks in the chat window. - -## Installation - -The recommended way of installing this plugin is to use Gajim's Plugin Installer. - -For more information and instruction on how to install plugins manually, please -refer to the [Gajim Plugin Wiki site](https://dev.gajim.org/gajim/gajim-plugins/wikis/home#how-to-install-plugins). - - -## Usage - -This plugin uses markdown-style syntax to identify which parts of a message -should be formatted as code in the chat window. - -``` -Inline source code will be highlighted when placed in between `two single -back-ticks`. -``` - -The language used to highlight the syntax of inline code is selected as the -default language in the plugin settings. - - -Multi-line code blocks are started by three back-ticks followed by a newline. -Optionally, a language can be specified directly after the opening back-ticks and -before the line break: -```` -```language -Note, that the last line of a code block may only contain the closing back-ticks, -i.e. there must be a newline here. -``` -```` - -In case no language is specified with the opening tag or the specified language -could not be identified, the default language configured in the settings is -used. - -You can test it by copying and sending the following text to one of your -contacts: -```` -```python -def test(): - print("Hello, world!") -``` -```` -(**Note:** your contact will not receive highlighted text unless she is also -using the plugin.) - - -## Relation to XEP-0393 - 'Message Styling' - -https://xmpp.org/extensions/xep-0393.html#pre-block - -In [XEP-0393](https://xmpp.org/extensions/xep-0393.html), -the back-tick based syntax is defined as markup for preformatted -text blocks, respectively inline preformatted text. -Formatting of such text blocks with mono-spaced fonts is recommended by the XEP. - -By using the same syntax as defined in XEP-0393 XMPP clients with only XEP-0393 -support but without syntax highlighting can at least present their users blocks -of preformatted text. - -Since text in between the back-tick markers is not further formatted by this -plugin, it can be considered "preformatted". -Hence, this plugin is compatible to the formatting options defined by XEP-0393, -[section 5.1.2, "Preformatted Text"](https://xmpp.org/extensions/xep-0393.html#pre-block) -and [section 5.2.5, "Preformatted Span"](https://xmpp.org/extensions/xep-0393.html#mono). - -Nevertheless, syntax highlighting for source code is not part of XEP but -rather a non-standard extension introduced with this plugin. - - -## Configuration - -The configuration can be found via 'Gajim' > 'Plugins', then select the -'Source Code Syntax Highlight' Plugin and click the gears symbol. -The configuration options let you specify many details how code is formatted, -including default language, style, font settings, background color and formatting -of the code markers. - -In the configuration window, the current settings are displayed in an -interactive preview panel. This allows you to directly check how code would -look like in the message -window. - -## Report Bugs and Feature Requests - -For bug reports, please report them to the [Gajim Plugin Issue tracker](https://dev.gajim.org/gajim/gajim-plugins/issues/new?issue[FlorianMuenchbach]=&issue[description]=Gajim%20Version%3A%20%0APlugin%20Version%3A%0AOperating%20System%3A&issue[title]=[syntax_highlight]). - -Please make sure that the issue you create contains `[syntax_highlight]` in the -title and information such as Gajim version, Plugin version, Operating system, -etc. - -## 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.p.syntax_highlight=DEBUG -``` -in a terminal to display the debug messages. - - -## Known Issues / ToDo - - * ~~Gajim crashes when correcting a message containing highlighted code.~~ - (fixed in version 1.1.0) - - -## Credits - -Since I had no experience in writing Plugins for Gajim, I used the -[Latex Plugin](https://dev.gajim.org/gajim/gajim-plugins/wikis/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/). diff --git a/syntax_highlight/chat_syntax_highlighter.py b/syntax_highlight/chat_syntax_highlighter.py index c18cc18..338dd62 100644 --- a/syntax_highlight/chat_syntax_highlighter.py +++ b/syntax_highlight/chat_syntax_highlighter.py @@ -8,12 +8,22 @@ from syntax_highlight.gtkformatter import GTKFormatter from syntax_highlight.types import MatchType from syntax_highlight.types import LineBreakOptions from syntax_highlight.types import CodeMarkerOptions +from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID log = logging.getLogger('gajim.p.syntax_highlight') class ChatSyntaxHighlighter: - def hide_code_markup(self, buf, start, end): + def __init__(self, plugin_config, highlighter_config, textview): + self.textview = textview + self._plugin_config = plugin_config + self._highlighter_config = highlighter_config + + def update_config(self, plugin_config): + self._plugin_config = plugin_config + + @staticmethod + def _hide_code_markup(buf, start, end): tag = buf.get_tag_table().lookup('hide_code_markup') if tag is None: tag = Gtk.TextTag.new('hide_code_markup') @@ -22,17 +32,16 @@ class ChatSyntaxHighlighter: buf.apply_tag_by_name('hide_code_markup', start, end) - def check_line_break(self, is_multiline): - line_break = self.config.get_line_break_action() - + def _check_line_break(self, is_multiline): + line_break = self._plugin_config['line_break'].value return (line_break == LineBreakOptions.ALWAYS) \ or (is_multiline and line_break == LineBreakOptions.MULTILINE) - def format_code(self, buf, s_tag, s_code, e_tag, e_code, language): - style = self.config.get_style_name() - if self.config.get_code_marker_setting() == CodeMarkerOptions.HIDE: - self.hide_code_markup(buf, s_tag, s_code) - self.hide_code_markup(buf, e_code, e_tag) + def _format_code(self, buf, s_tag, s_code, e_tag, e_code, language): + style = self._plugin_config['style'] + if self._plugin_config['code_marker'] == CodeMarkerOptions.HIDE: + self._hide_code_markup(buf, s_tag, s_code) + self._hide_code_markup(buf, e_code, e_tag) else: comment_tag = GTKFormatter.create_tag_for_token( pygments.token.Comment, @@ -49,24 +58,25 @@ class ChatSyntaxHighlighter: lexer = None if language is None: - lexer = self.config.get_default_lexer() + lexer = self._highlighter_config.get_default_lexer() log.info('No Language specified. ' 'Falling back to default lexer: %s.', - self.config.get_default_lexer_name()) + self._highlighter_config.get_default_lexer_name()) else: log.debug('Using lexer for %s.', str(language)) - lexer = self.config.get_lexer_with_fallback(language) + lexer = self._highlighter_config.get_lexer_with_fallback(language) if lexer is None: iterator = buf.get_iter_at_mark(start_mark) buf.insert(iterator, '\n') - elif not self.config.is_internal_none_lexer(lexer): + elif lexer != PLUGIN_INTERNAL_NONE_LEXER_ID: tokens = pygments.lex(code, lexer) formatter = GTKFormatter(style=style, start_mark=start_mark) pygments.format(tokens, formatter, buf) - def find_multiline_matches(self, text): + @staticmethod + def _find_multiline_matches(text): start = None matches = [] # Less strict, allow prefixed whitespaces: @@ -84,7 +94,8 @@ class ChatSyntaxHighlighter: continue return matches - def find_inline_matches(self, text): + @staticmethod + def _find_inline_matches(text): """ Inline code is highlighted if the start marker is precedded by a start of line, a whitespace character or either of the other span markers @@ -95,18 +106,19 @@ class ChatSyntaxHighlighter: re.finditer(r'(?:^|\s|\*|~|_)(`((?!`).+?)`)(?:\s|\*|~|_|$)', text)] - def merge_match_groups(self, real_text, inline_matches, multiline_matches): + @staticmethod + def _merge_match_groups(real_text, inline_matches, multiline_matches): it_inline = iter(inline_matches) it_multi = iter(multiline_matches) length = len(real_text) # Just to get cleaner code below... - def get_next(iterator): + def _get_next(iterator): return next(iterator, (length, length, '')) # In order to simplify the process, we use the 'length' here. - cur_inline = get_next(it_inline) - cur_multi = get_next(it_multi) + cur_inline = _get_next(it_inline) + cur_multi = _get_next(it_multi) pos = 0 @@ -142,18 +154,18 @@ class ChatSyntaxHighlighter: # Also, forward the other one, if regions overlap or we took over... if selected[2] == MatchType.INLINE: if cur_multi[0] < cur_inline[1]: - cur_multi = get_next(it_multi) - cur_inline = get_next(it_inline) + cur_multi = _get_next(it_multi) + cur_inline = _get_next(it_inline) elif selected[2] == MatchType.MULTILINE: if cur_inline[0] < cur_multi[1]: - cur_inline = get_next(it_inline) - cur_multi = get_next(it_multi) + cur_inline = _get_next(it_inline) + cur_multi = _get_next(it_multi) return parts def process_text(self, real_text, other_tags, _graphics, iter_, - _additional): - def fix_newline(char, marker_len_no_newline, force=False): + _additional): + def _fix_newline(char, marker_len_no_newline, force=False): fixed = (marker_len_no_newline, '') if char == '\n': fixed = (marker_len_no_newline + 1, '') @@ -164,8 +176,8 @@ class ChatSyntaxHighlighter: buf = self.textview.tv.get_buffer() # First, try to find inline or multiline code snippets - inline_matches = self.find_inline_matches(real_text) - multiline_matches = self.find_multiline_matches(real_text) + inline_matches = self._find_inline_matches(real_text) + multiline_matches = self._find_multiline_matches(real_text) if not inline_matches and not multiline_matches: log.debug('Stopping early, since there is no code block in it...') @@ -177,10 +189,10 @@ class ChatSyntaxHighlighter: start_mark = buf.create_mark('SHP_start', iterator, True) end_mark = buf.create_mark('SHP_end', iterator, False) - insert_newline_for_multiline = self.check_line_break(True) - insert_newline_for_inline = self.check_line_break(False) + insert_newline_for_multiline = self._check_line_break(True) + insert_newline_for_inline = self._check_line_break(False) - split_text = self.merge_match_groups( + split_text = self._merge_match_groups( real_text, inline_matches, multiline_matches) buf.begin_user_action() @@ -204,20 +216,20 @@ class ChatSyntaxHighlighter: language_len = 0 if language is None else len(language) # We account the language word width for the front marker - front = fix_newline( + front = _fix_newline( text_to_insert[0], 3 + language_len, insert_newline_for_multiline) - back = fix_newline( + back = _fix_newline( text_to_insert[-1], 3, insert_newline_for_multiline and not end_of_message) else: - front = fix_newline( + front = _fix_newline( text_to_insert[0], 1, insert_newline_for_inline) - back = fix_newline( + back = _fix_newline( text_to_insert[-1], 1, insert_newline_for_inline and not end_of_message) @@ -226,8 +238,9 @@ class ChatSyntaxHighlighter: text_to_insert = ''.join([front[1], text_to_insert, back[1]]) # Insertion invalidates iterator, let's use our start mark... - self.insert_and_format_code(buf, text_to_insert, language, - marker_widths, start_mark, end_mark, other_tags) + self._insert_and_format_code( + buf, text_to_insert, language, marker_widths, start_mark, + end_mark, other_tags) iterator = buf.get_iter_at_mark(end_mark) # The current end of the buffer's contents is the start for the @@ -244,14 +257,13 @@ class ChatSyntaxHighlighter: # print_special_text method is resetting the plugin_modified variable... self.textview.plugin_modified = True - def insert_and_format_code(self, buf, insert_text, language, marker, - start_mark, end_mark, other_tags=None): + def _insert_and_format_code(self, buf, insert_text, language, marker, + start_mark, end_mark, other_tags=None): start_iter = buf.get_iter_at_mark(start_mark) if other_tags: - buf.insert_with_tags_by_name(start_iter, insert_text, - *other_tags) + buf.insert_with_tags_by_name(start_iter, insert_text, *other_tags) else: buf.insert(start_iter, insert_text) @@ -264,20 +276,16 @@ class ChatSyntaxHighlighter: log.debug('full text between tags: %s.', tag_start.get_text(tag_end)) - self.format_code(buf, tag_start, s_code, tag_end, e_code, language) + self._format_code(buf, tag_start, s_code, tag_end, e_code, language) self.textview.plugin_modified = True # Set general code block format tag = Gtk.TextTag.new() - if self.config.is_bgcolor_override_enabled(): - tag.set_property('background', self.config.get_bgcolor()) - tag.set_property('paragraph-background', self.config.get_bgcolor()) - tag.set_property('font', self.config.get_font()) + bg_color = self._plugin_config['bgcolor'] + if self._plugin_config['bgcolor_override']: + tag.set_property('background', bg_color) + tag.set_property('paragraph-background', bg_color) + tag.set_property('font', self._plugin_config['font']) buf.get_tag_table().add(tag) buf.apply_tag(tag, tag_start, tag_end) - - def __init__(self, config, textview): - self.last_end_mark = None - self.config = config - self.textview = textview diff --git a/syntax_highlight/config_dialog.py b/syntax_highlight/config_dialog.py new file mode 100644 index 0000000..0cfc95e --- /dev/null +++ b/syntax_highlight/config_dialog.py @@ -0,0 +1,206 @@ +import logging +import re +import math +from pathlib import Path +import pygments + +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository.Pango import FontDescription +from gi.repository.Pango import Style +from gi.repository.Pango import SCALE + +from gajim.common import app + +from gajim.plugins.plugins_i18n import _ +from gajim.plugins.helpers import get_builder + +from syntax_highlight.gtkformatter import GTKFormatter +from syntax_highlight.types import LineBreakOptions +from syntax_highlight.types import CodeMarkerOptions +from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID + +log = logging.getLogger('gajim.p.syntax_highlight') + +PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)', + PLUGIN_INTERNAL_NONE_LEXER_ID) + + +class SyntaxHighlighterPluginConfig(Gtk.ApplicationWindow): + def __init__(self, plugin, transient): + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_show_menubar(False) + self.set_title(_('Syntax Highlighter Configuration')) + self.set_transient_for(transient) + self.set_default_size(400, 500) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_modal(True) + self.set_destroy_with_parent(True) + + ui_path = Path(__file__).parent + self._ui = get_builder(ui_path.resolve() / 'config_dialog.ui') + self.add(self._ui.main_box) + self.show_all() + + self._ui.preview_textview.get_buffer().connect( + 'insert-text', self._on_preview_text_inserted) + self._ui.connect_signals(self) + + self._lexer_liststore = Gtk.ListStore(str) + self._ui.default_lexer_combobox.set_model(self._lexer_liststore) + + self._style_liststore = Gtk.ListStore(str) + self._ui.style_combobox.set_model(self._style_liststore) + + self._plugin = plugin + self._lexers = plugin.highlighter_config.get_lexer_list() + self._styles = plugin.highlighter_config.get_styles_list() + + self._provider = None + self._add_css_provider() + + self._initialize() + + def _initialize(self): + default_lexer = self._plugin.highlighter_config.get_default_lexer_name() + + for i, lexer in enumerate(self._lexers): + self._lexer_liststore.append([lexer[0]]) + if lexer[1] == default_lexer: + self._ui.default_lexer_combobox.set_active(i) + + for i, style in enumerate(self._styles): + self._style_liststore.append([style]) + if style == self._plugin.config['style']: + self._ui.style_combobox.set_active(i) + + self._ui.line_break_combobox.set_active( + self._plugin.config['line_break'].value) + self._ui.code_marker_combobox.set_active( + self._plugin.config['code_marker']) + self._ui.font_button.set_font(self._plugin.config['font']) + + bg_override_enabled = self._plugin.config['bgcolor_override'] + self._ui.bg_color_checkbutton.set_active(bg_override_enabled) + self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled) + color = Gdk.RGBA() + if color.parse(self._plugin.config['bgcolor']): + self._ui.bg_color_colorbutton.set_rgba(color) + self._update_preview() + + def _lexer_changed(self, widget): + self._plugin.highlighter_config.set_default_lexer( + self._lexers[widget.get_active()][1]) + self._update_preview() + + def _line_break_changed(self, widget): + self._plugin.config['line_break'] = LineBreakOptions( + widget.get_active()) + self._update_preview() + + def _code_marker_changed(self, widget): + self._plugin.config['code_marker'] = CodeMarkerOptions( + widget.get_active()) + + def _bg_color_enabled(self, widget): + override_color = widget.get_active() + self._plugin.config['bgcolor_override'] = override_color + self._ui.bg_color_colorbutton.set_sensitive(override_color) + self._update_preview() + + def _bg_color_changed(self, widget): + color = widget.get_rgba() + self._plugin.config['bgcolor'] = color.to_string() + self._update_preview() + + def _style_changed(self, widget): + style = self._styles[widget.get_active()] + if style is not None and style != '': + self._plugin.config['style'] = style + self._update_preview() + + def _font_changed(self, widget): + font = widget.get_font() + if font is not None and font != '': + self._plugin.config['font'] = font + self._update_preview() + + def _update_preview(self): + self._format_preview_text() + + def _on_preview_text_inserted(self, _buf, _iterator, text, length, *_args): + if (length == 1 and re.match(r'\s', text)) or length > 1: + self._format_preview_text() + + def _add_css_provider(self): + self._context = self._ui.preview_textview.get_style_context() + self._provider = Gtk.CssProvider() + self._context.add_provider( + self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + self._context.add_class('syntax-preview') + + def _format_preview_text(self): + buf = self._ui.preview_textview.get_buffer() + start_iter = buf.get_start_iter() + start_mark = buf.create_mark(None, start_iter, True) + buf.remove_all_tags(start_iter, buf.get_end_iter()) + + formatter = GTKFormatter( + style=self._plugin.config['style'], start_mark=start_mark) + + code = start_iter.get_text(buf.get_end_iter()) + lexer = self._plugin.highlighter_config.get_default_lexer() + if lexer != PLUGIN_INTERNAL_NONE_LEXER_ID: + tokens = pygments.lex(code, lexer) + pygments.format(tokens, formatter, buf) + + buf.delete_mark(start_mark) + css = self._get_css() + self._provider.load_from_data(bytes(css.encode())) + + def _get_css(self): + # Build CSS from Pango.FontDescription + description = FontDescription.from_string(self._plugin.config['font']) + size = description.get_size() / SCALE + style = self._get_string_from_pango_style(description.get_style()) + weight = self._pango_to_css_weight(int(description.get_weight())) + family = description.get_family() + font = '%spt %s' % (size, family) + + if self._plugin.config['bgcolor_override']: + color = self._plugin.config['bgcolor'] + else: + color = '@theme_base_color' + + css = ''' + .syntax-preview { + font: %s; + font-weight: %s; + font-style: %s; + } + .syntax-preview > text { + background-color: %s; + } + ''' % (font, weight, style, color) + return css + + @staticmethod + def _pango_to_css_weight(number): + # Pango allows for weight values between 100 and 1000 + # CSS allows only full hundred numbers like 100, 200 .. + number = int(number) + if number < 100: + return 100 + if number > 900: + return 900 + return int(math.ceil(number / 100.0)) * 100 + + @staticmethod + def _get_string_from_pango_style(style: Style) -> str: + if style == Style.NORMAL: + return 'normal' + if style == Style.ITALIC: + return 'italic' + # Style.OBLIQUE: + return 'oblique' diff --git a/syntax_highlight/config_dialog.ui b/syntax_highlight/config_dialog.ui index 64328d3..09a2764 100644 --- a/syntax_highlight/config_dialog.ui +++ b/syntax_highlight/config_dialog.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.22.1 --> +<!-- Generated with glade 3.36.0 --> <interface> - <requires lib="gtk+" version="3.20"/> + <requires lib="gtk+" version="3.22"/> <object class="GtkTextBuffer"/> <object class="GtkListStore" id="code_marker_selection"> <columns> @@ -309,7 +309,4 @@ </packing> </child> </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> </interface> diff --git a/syntax_highlight/gtkformatter.py b/syntax_highlight/gtkformatter.py index d7347f0..823f1ad 100644 --- a/syntax_highlight/gtkformatter.py +++ b/syntax_highlight/gtkformatter.py @@ -67,7 +67,7 @@ class GTKFormatter(Formatter): def format(self, tokensource, outfile): if not isinstance(outfile, Gtk.TextBuffer) or outfile is None: - log.warn("Did not get a buffer to format...") + log.warning('Did not get a buffer to format...') return buf = outfile diff --git a/syntax_highlight/highlighter_config.py b/syntax_highlight/highlighter_config.py new file mode 100644 index 0000000..061ccee --- /dev/null +++ b/syntax_highlight/highlighter_config.py @@ -0,0 +1,97 @@ +import logging + +from pygments.lexers import get_lexer_by_name +from pygments.lexers import get_all_lexers +from pygments.styles import get_all_styles +from pygments.util import ClassNotFound + +from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID + +log = logging.getLogger('gajim.p.syntax_highlight') +PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)', + PLUGIN_INTERNAL_NONE_LEXER_ID) + + +class HighlighterConfig: + def __init__(self, plugin_config): + self._plugin_config = plugin_config + + self._lexer_list = self._create_lexer_list() + self._style_list = [] + for style in get_all_styles(): + self._style_list.append(style) + self._style_list.sort() + + self._default_lexer = None + self.set_default_lexer(self._plugin_config['default_lexer']) + + @staticmethod + def _create_lexer_list(): + # The list we create here contains the plain text name and the lexer's + # id string + lexers = [] + + # Iteration over get_all_lexers() seems to be broken somehow + # Workaround + all_lexers = get_all_lexers() + for lexer in all_lexers: + # We don't want to add lexers that we cant identify by name later + if lexer[1] is not None and lexer[1]: + lexers.append((lexer[0], lexer[1][0])) + lexers.sort() + + # Insert our internal 'none' type at top of the list + lexers.insert(0, PLUGIN_INTERNAL_NONE_LEXER) + return lexers + + @staticmethod + def get_lexer_by_name(name): + lexer = None + try: + lexer = get_lexer_by_name(name) + except ClassNotFound: + pass + return lexer + + def get_lexer_with_fallback(self, language): + lexer = self.get_lexer_by_name(language) + if lexer is None: + log.info('Falling back to default lexer for %s.', + self.get_default_lexer_name()) + lexer = self._default_lexer[1] + return lexer + + def set_default_lexer(self, name): + if name != PLUGIN_INTERNAL_NONE_LEXER_ID: + lexer = get_lexer_by_name(name) + + if lexer is None and self._default_lexer is None: + log.error('Failed to get default lexer by name.' + 'Falling back to simply using the first lexer ' + 'in the list.') + lexer = self._lexer_list[0] + name = lexer[0] + self._default_lexer = (name, lexer) + if lexer is None and self._default_lexer is not None: + log.info('Failed to get default lexer by name, keeping ' + 'previous setting (lexer = %s).', + self._default_lexer[0]) + name = self._default_lexer[0] + else: + self._default_lexer = (name, lexer) + else: + self._default_lexer = PLUGIN_INTERNAL_NONE_LEXER + + self._plugin_config['default_lexer'] = name + + def get_default_lexer(self): + return self._default_lexer[1] + + def get_default_lexer_name(self): + return self._default_lexer[0] + + def get_lexer_list(self): + return self._lexer_list + + def get_styles_list(self): + return self._style_list diff --git a/syntax_highlight/plugin_config.py b/syntax_highlight/plugin_config.py deleted file mode 100644 index ebed683..0000000 --- a/syntax_highlight/plugin_config.py +++ /dev/null @@ -1,168 +0,0 @@ -import logging - -from gi.repository import Gdk - -from pygments.lexers import get_lexer_by_name -from pygments.lexers import get_all_lexers -from pygments.styles import get_all_styles -from pygments.util import ClassNotFound - -from syntax_highlight.types import LineBreakOptions -from syntax_highlight.types import CodeMarkerOptions -from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID - -log = logging.getLogger('gajim.p.syntax_highlight') - - -class SyntaxHighlighterConfig: - PLUGIN_INTERNAL_NONE_LEXER = ('None (monospace only)', - PLUGIN_INTERNAL_NONE_LEXER_ID) - - def _create_lexer_list(self): - # The list we create here contains the plain text name and the lexer's - # id string - lexers = [] - - # Iteration over get_all_lexers() seems to be broken somehow - # Workaround - all_lexers = get_all_lexers() - for lexer in all_lexers: - # We don't want to add lexers that we cant identify by name later - if lexer[1] is not None and lexer[1]: - lexers.append((lexer[0], lexer[1][0])) - lexers.sort() - - # Insert our internal 'none' type at top of the list - lexers.insert(0, self.PLUGIN_INTERNAL_NONE_LEXER) - return lexers - - def is_internal_none_lexer(self, lexer): - return lexer == PLUGIN_INTERNAL_NONE_LEXER_ID - - def get_internal_none_lexer(self): - return self.PLUGIN_INTERNAL_NONE_LEXER - - def get_lexer_by_name(self, name): - lexer = None - try: - lexer = get_lexer_by_name(name) - except ClassNotFound: - pass - return lexer - - def get_lexer_with_fallback(self, language): - lexer = self.get_lexer_by_name(language) - if lexer is None: - log.info('Falling back to default lexer for %s.', - self.get_default_lexer_name()) - lexer = self.default_lexer[1] - return lexer - - def set_font(self, font): - if font is not None and font != '': - self.config['font'] = font - - def set_style(self, style): - if style is not None and style != '': - self.config['style'] = style - - def set_line_break_action(self, option): - if isinstance(option, int): - option = LineBreakOptions(option) - self.config['line_break'] = option - - def set_default_lexer(self, name): - if not self.is_internal_none_lexer(name): - lexer = get_lexer_by_name(name) - - if lexer is None and self.default_lexer is None: - log.error('Failed to get default lexer by name.' - 'Falling back to simply using the first lexer ' - 'in the list.') - lexer = self.lexer_list[0] - name = lexer[0] - self.default_lexer = (name, lexer) - if lexer is None and self.default_lexer is not None: - log.info('Failed to get default lexer by name, keeping ' - 'previous setting (lexer = %s).', - self.default_lexer[0]) - name = self.default_lexer[0] - else: - self.default_lexer = (name, lexer) - else: - self.default_lexer = self.PLUGIN_INTERNAL_NONE_LEXER - - self.config['default_lexer'] = name - - def set_bgcolor_override_enabled(self, state): - self.config['bgcolor_override'] = state - - def set_bgcolor(self, color): - if isinstance(color, Gdk.RGBA): - color = color.to_string() - self.config['bgcolor'] = color - - def set_code_marker_setting(self, option): - if isinstance(option, int): - option = CodeMarkerOptions(option) - self.config['code_marker'] = option - - def set_pygments_path(self, path): - self.config['pygments_path'] = path - - def get_default_lexer(self): - return self.default_lexer[1] - - def get_default_lexer_name(self): - return self.default_lexer[0] - - def get_lexer_list(self): - return self.lexer_list - - def get_line_break_action(self): - # Return int only - if isinstance(self.config['line_break'], int): - # In case of legacy settings, convert. - action = self.config['line_break'] - self.set_line_break_action(action) - else: - action = self.config['line_break'].value - - return action - - def get_pygments_path(self): - return self.config['pygments_path'] - - def get_font(self): - return self.config['font'] - - def get_style_name(self): - return self.config['style'] - - def is_bgcolor_override_enabled(self): - return self.config['bgcolor_override'] - - def get_bgcolor(self): - return self.config['bgcolor'] - - def get_code_marker_setting(self): - return self.config['code_marker'] - - def get_styles_list(self): - return self.style_list - - def init_pygments(self): - """ - Initialize all config variables that depend directly on pygments being - available. - """ - self.lexer_list = self._create_lexer_list() - self.style_list = [s for s in get_all_styles()] - self.style_list.sort() - self.set_default_lexer(self.config['default_lexer']) - - def __init__(self, config): - self.lexer_list = [] - self.style_list = [] - self.config = config - self.default_lexer = None diff --git a/syntax_highlight/plugin_config_dialog.py b/syntax_highlight/plugin_config_dialog.py deleted file mode 100644 index c33e2b1..0000000 --- a/syntax_highlight/plugin_config_dialog.py +++ /dev/null @@ -1,151 +0,0 @@ -import re -import pygments - -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository.Pango import FontDescription - -from gajim.plugins.gui import GajimPluginConfigDialog -from gajim.plugins.helpers import get_builder - -from syntax_highlight.gtkformatter import GTKFormatter -from syntax_highlight.types import LineBreakOptions -from syntax_highlight.types import CodeMarkerOptions - - -class SyntaxHighlighterPluginConfiguration(GajimPluginConfigDialog): - def init(self): - path = self.plugin.local_file_path('config_dialog.ui') - self._ui = get_builder(path) - box = self.get_content_area() - box.pack_start(self._ui.main_box, True, True, 0) - - self._ui.set_translation_domain('gajim_plugins') - - self.liststore = Gtk.ListStore(str) - self._ui.default_lexer_combobox.set_model(self.liststore) - - self.style_liststore = Gtk.ListStore(str) - self._ui.style_combobox.set_model(self.style_liststore) - - self._ui.preview_textview.get_buffer().connect( - 'insert-text', self._on_preview_text_inserted) - - self._ui.connect_signals(self) - - self.default_lexer_id = 0 - self.style_id = 0 - - def set_config(self, config): - self.config = config - self.lexers = self.config.get_lexer_list() - self.styles = self.config.get_styles_list() - default_lexer = self.config.get_default_lexer_name() - default_style = self.config.get_style_name() - - for i, lexer in enumerate(self.lexers): - self.liststore.append([lexer[0]]) - if lexer[1] == default_lexer: - self.default_lexer_id = i - - for i, style in enumerate(self.styles): - self.style_liststore.append([style]) - if style == default_style: - self.style_id = i - self._update_preview() - - def _lexer_changed(self, _widget): - new = self._ui.default_lexer_combobox.get_active() - if new != self.default_lexer_id: - self.default_lexer_id = new - self.config.set_default_lexer(self.lexers[self.default_lexer_id][1]) - self._update_preview() - - def _line_break_changed(self, _widget): - new = LineBreakOptions(self._ui.line_break_combobox.get_active()) - if new != self.config.get_line_break_action(): - self.config.set_line_break_action(new) - self._update_preview() - - def _code_marker_changed(self, _widget): - new = CodeMarkerOptions(self._ui.code_marker_combobox.get_active()) - if new != self.config.get_code_marker_setting(): - self.config.set_code_marker_setting(new) - - def _bg_color_enabled(self, _widget): - new = self._ui.bg_color_checkbutton.get_active() - if new != self.config.is_bgcolor_override_enabled(): - bg_override_enabled = new - self.config.set_bgcolor_override_enabled(bg_override_enabled) - self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled) - self._update_preview() - - def _bg_color_changed(self, _widget): - new = self._ui.bg_color_colorbutton.get_rgba() - if new != self.config.get_bgcolor(): - self.config.set_bgcolor(new) - self._update_preview() - - def _style_changed(self, _widget): - new = self._ui.style_combobox.get_active() - if new != self.style_id: - self.style_id = new - self.config.set_style(self.styles[self.style_id]) - self._update_preview() - - def _font_changed(self, _widget): - new = self._ui.font_button.get_font() - if new != self.config.get_font(): - self.config.set_font(new) - self._update_preview() - - def _update_preview(self): - self._format_preview_text() - - def _on_preview_text_inserted(self, _buf, _iterator, text, length, *_args): - if (length == 1 and re.match(r'\s', text)) or length > 1: - self._format_preview_text() - - def _format_preview_text(self): - buf = self._ui.preview_textview.get_buffer() - start_iter = buf.get_start_iter() - start_mark = buf.create_mark(None, start_iter, True) - buf.remove_all_tags(start_iter, buf.get_end_iter()) - - formatter = GTKFormatter( - style=self.config.get_style_name(), start_mark=start_mark) - - code = start_iter.get_text(buf.get_end_iter()) - lexer = self.config.get_default_lexer() - if not self.config.is_internal_none_lexer(lexer): - tokens = pygments.lex(code, lexer) - pygments.format(tokens, formatter, buf) - - buf.delete_mark(start_mark) - - self._ui.preview_textview.override_font( - FontDescription.from_string(self.config.get_font())) - - color = Gdk.RGBA() - if color.parse(self.config.get_bgcolor()): - self._ui.preview_textview.override_background_color( - Gtk.StateFlags.NORMAL, color) - - def on_run(self): - self._ui.default_lexer_combobox.set_active(self.default_lexer_id) - self._ui.line_break_combobox.set_active( - self.config.get_line_break_action()) - self._ui.code_marker_combobox.set_active( - self.config.get_code_marker_setting()) - self._ui.style_combobox.set_active(self.style_id) - - self._ui.font_button.set_font(self.config.get_font()) - - bg_override_enabled = self.config.is_bgcolor_override_enabled() - self._ui.bg_color_checkbutton.set_active(bg_override_enabled) - - self._ui.bg_color_colorbutton.set_sensitive(bg_override_enabled) - - color = Gdk.RGBA() - if color.parse(self.config.get_bgcolor()): - self._ui.bg_color_colorbutton.set_rgba(color) diff --git a/syntax_highlight/syntax_highlight.py b/syntax_highlight/syntax_highlight.py index c041d02..00190c8 100644 --- a/syntax_highlight/syntax_highlight.py +++ b/syntax_highlight/syntax_highlight.py @@ -1,111 +1,91 @@ import logging -import sys +from functools import partial from gajim.plugins import GajimPlugin +from gajim.plugins.plugins_i18n import _ from syntax_highlight.types import LineBreakOptions from syntax_highlight.types import CodeMarkerOptions from syntax_highlight.types import PLUGIN_INTERNAL_NONE_LEXER_ID -if sys.version_info >= (3, 4): - from importlib.util import find_spec as find_module -else: - from importlib import find_loader as find_module - -PYGMENTS_MISSING = 'You are missing Python-Pygments.' log = logging.getLogger('gajim.p.syntax_highlight') - -def try_loading_pygments(): - success = find_module('pygments') is not None - if success: - try: - from syntax_highlight.chat_syntax_highlighter import \ - ChatSyntaxHighlighter - from syntax_highlight.plugin_config_dialog import \ - SyntaxHighlighterPluginConfiguration - from syntax_highlight.plugin_config import SyntaxHighlighterConfig - global SyntaxHighlighterPluginConfiguration - global ChatSyntaxHighlighter - global SyntaxHighlighterConfig - success = True - log.debug("pygments loaded.") - except Exception as exception: - log.error("Import Error: %s.", exception) - success = False - - return success +HAS_PYGMENTS = False +try: + from syntax_highlight.chat_syntax_highlighter import ChatSyntaxHighlighter + from syntax_highlight.config_dialog import SyntaxHighlighterPluginConfig + from syntax_highlight.highlighter_config import HighlighterConfig + HAS_PYGMENTS = True +except Exception as exception: + log.error('Could not load pygments: %s', exception) class SyntaxHighlighterPlugin(GajimPlugin): - def on_connect_with_chat_control(self, chat_control): - account = chat_control.contact.account.name - jid = chat_control.contact.jid - if account not in self.ccontrol: - self.ccontrol[account] = {} - self.ccontrol[account][jid] = ChatSyntaxHighlighter( - self.conf, chat_control.conv_textview) - - def on_disconnect_from_chat_control(self, chat_control): - account = chat_control.contact.account.name - jid = chat_control.contact.jid - del self.ccontrol[account][jid] - - def on_print_real_text(self, text_view, real_text, other_tags, graphics, - iterator, additional): - account = text_view.account - for jid in self.ccontrol[account]: - if self.ccontrol[account][jid].textview != text_view: - continue - self.ccontrol[account][jid].process_text( - real_text, other_tags, graphics, iterator, additional) - return - - def try_init(self): - """ - Separating this part of the initialization from the init() method - allows repeating this step again, without reloading the plugin, - i.e. restarting Gajim for instance. - Doing so allows resolving the dependency issues without restart :) - """ - pygments_loaded = try_loading_pygments() - if not pygments_loaded: - return False - - self.activatable = True - self.available_text = None - self.config_dialog = SyntaxHighlighterPluginConfiguration(self) - - self.conf = SyntaxHighlighterConfig(self.config) - # The following initialization requires pygments to be available. - self.conf.init_pygments() - - self.config_dialog = SyntaxHighlighterPluginConfiguration(self) - self.config_dialog.set_config(self.conf) - - self.gui_extension_points = { - 'chat_control_base': ( - self.on_connect_with_chat_control, - self.on_disconnect_from_chat_control), - 'print_real_text': (self.on_print_real_text, None), } - return True - def init(self): - self.ccontrol = {} + self.description = _( + 'Source code syntax highlighting in the chat window.\n\n' + 'Markdown-style syntax is supported, i.e. text inbetween ' + '`single backticks` is rendered as inline code.\n' + '```language\n' + 'selection is possible in multi-line code snippets inbetween ' + 'triple-backticks\n' + 'Note the newlines in this case…\n' + '```\n\n' + 'Changed settings will take effect after re-opening the message ' + 'tab/window.') self.config_default_values = { 'default_lexer': (PLUGIN_INTERNAL_NONE_LEXER_ID, ''), 'line_break': (LineBreakOptions.MULTILINE, ''), 'style': ('default', ''), 'font': ('Monospace 10', ''), - 'bgcolor': ('#ccc', ''), + 'bgcolor': ('rgb(200, 200, 200)', ''), 'bgcolor_override': (True, ''), 'code_marker': (CodeMarkerOptions.AS_COMMENT, ''), - 'pygments_path': (None, ''), } + } - is_initialized = self.try_init() + self.gui_extension_points = { + 'chat_control_base': ( + self._connect_chat_control, + self._disconnect_chat_control), + 'print_real_text': (self._on_print_real_text, None) + } - if not is_initialized: + if not HAS_PYGMENTS: self.activatable = False - self.available_text = PYGMENTS_MISSING + self.available_text = _('You are missing python-pygments.') self.config_dialog = None + + self._migrate_settings() + self._highlighters = {} + self.config_dialog = partial(SyntaxHighlighterPluginConfig, self) + self.highlighter_config = HighlighterConfig(self.config) + + def _migrate_settings(self): + line_break = self.config['line_break'] + if isinstance(line_break, int): + self.config['line_break'] = LineBreakOptions(line_break) + + def _connect_chat_control(self, chat_control): + highlighter = ChatSyntaxHighlighter( + self.config, self.highlighter_config, chat_control.conv_textview) + self._highlighters[chat_control.control_id] = highlighter + + def _disconnect_chat_control(self, chat_control): + highlighter = self._highlighters.get(chat_control.control_id) + if highlighter is not None: + del highlighter + self._highlighters.pop(chat_control.control_id, None) + + def _on_print_real_text(self, text_view, real_text, other_tags, graphics, + iterator, additional): + for highlighter in self._highlighters.values(): + if highlighter.textview != text_view: + continue + highlighter.process_text( + real_text, other_tags, graphics, iterator, additional) + return + + def update_highlighters(self): + for highlighter in self._highlighters.values(): + highlighter.update_config(self.config) |