apostrophe/apostrophe/text_view_markup_handler.py

387 lines
17 KiB
Python
Raw Normal View History

# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
2019-07-05 18:47:49 +00:00
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import re
from multiprocessing import Pipe, Process
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
import gi
from apostrophe import helpers, markup_regex
from apostrophe.markup_regex import STRIKETHROUGH, BOLD_ITALIC, BOLD, ITALIC_ASTERISK, ITALIC_UNDERSCORE, IMAGE, LINK,\
LINK_ALT, HORIZONTAL_RULE, LIST, ORDERED_LIST, BLOCK_QUOTE, HEADER, HEADER_UNDER, TABLE, MATH, \
CODE
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
from gi.repository import Pango
class MarkupHandler:
TAG_NAME_ITALIC = 'italic'
TAG_NAME_BOLD = 'bold'
TAG_NAME_BOLD_ITALIC = 'bold_italic'
TAG_NAME_STRIKETHROUGH = 'strikethrough'
TAG_NAME_CENTER = 'center'
TAG_NAME_WRAP_NONE = 'wrap_none'
TAG_NAME_PLAIN_TEXT = 'plain_text'
TAG_NAME_GRAY_TEXT = 'gray_text'
TAG_NAME_CODE_TEXT = 'code_text'
TAG_NAME_CODE_BLOCK = 'code_block'
TAG_NAME_UNFOCUSED_TEXT = 'unfocused_text'
TAG_NAME_MARGIN_INDENT = 'margin_indent'
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
self.marked_up_text = None
# Tags.
buffer = self.text_buffer
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
self.tag_italic = buffer.create_tag(self.TAG_NAME_ITALIC,
weight=Pango.Weight.NORMAL,
style=Pango.Style.ITALIC)
self.tag_bold = buffer.create_tag(self.TAG_NAME_BOLD,
weight=Pango.Weight.BOLD,
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
style=Pango.Style.NORMAL)
self.tag_bold_italic = buffer.create_tag(self.TAG_NAME_BOLD_ITALIC,
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.tag_strikethrough = buffer.create_tag(self.TAG_NAME_STRIKETHROUGH,
strikethrough=True)
self.tag_center = buffer.create_tag(self.TAG_NAME_CENTER,
justification=Gtk.Justification.CENTER)
self.tag_wrap_none = buffer.create_tag(self.TAG_NAME_WRAP_NONE,
wrap_mode=Gtk.WrapMode.NONE,
pixels_above_lines=0,
pixels_below_lines=0)
self.tag_plain_text = buffer.create_tag(self.TAG_NAME_PLAIN_TEXT,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False,
justification=Gtk.Justification.LEFT)
self.tag_gray_text = buffer.create_tag(self.TAG_NAME_GRAY_TEXT,
foreground='gray',
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL)
self.tag_code_text = buffer.create_tag(self.TAG_NAME_CODE_TEXT,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False)
self.tag_code_block = buffer.create_tag(self.TAG_NAME_CODE_BLOCK,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False,
indent=self.get_margin_indent(0, 1)[1])
self.tags_markup = {
self.TAG_NAME_ITALIC: lambda args: self.tag_italic,
self.TAG_NAME_BOLD: lambda args: self.tag_bold,
self.TAG_NAME_BOLD_ITALIC: lambda args: self.tag_bold_italic,
self.TAG_NAME_STRIKETHROUGH: lambda args: self.tag_strikethrough,
self.TAG_NAME_CENTER: lambda args: self.tag_center,
self.TAG_NAME_WRAP_NONE: lambda args: self.tag_wrap_none,
self.TAG_NAME_PLAIN_TEXT: lambda args: self.tag_plain_text,
self.TAG_NAME_GRAY_TEXT: lambda args: self.tag_gray_text,
self.TAG_NAME_CODE_TEXT: lambda args: self.tag_code_text,
self.TAG_NAME_CODE_BLOCK: lambda args: self.tag_code_block,
self.TAG_NAME_MARGIN_INDENT: lambda args: self.get_margin_indent_tag(*args)
}
# Focus mode.
self.tag_unfocused_text = buffer.create_tag(self.TAG_NAME_UNFOCUSED_TEXT,
foreground='gray',
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL)
# Margin and indents.
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc.
self.tags_margins_indents = {}
self.baseline_margin = 0
self.char_width = 0
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
self.update_margins_indents()
# Style.
self.on_style_updated()
# Worker process to handle parsing.
self.parsing = False
self.apply_pending = False
self.parent_conn, child_conn = Pipe()
Process(target=self.parse, args=(child_conn,), daemon=True).start()
GLib.io_add_watch(
self.parent_conn.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.on_parsed)
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
def on_style_updated(self, *_):
style_context = self.text_view.get_style_context()
(found, color) = style_context.lookup_color('code_bg_color')
if not found:
(_, color) = style_context.lookup_color('background_color')
self.tag_code_text.set_property("background", color.to_string())
self.tag_code_block.set_property("paragraph-background", color.to_string())
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
def apply(self):
"""Applies markup, parsing it in a worker process if the text has changed.
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
In case parsing is already running, it will re-apply once it finishes. This ensure that
the pipe doesn't fill (and block) if multiple requests are made in quick succession."""
if not self.parsing:
self.parsing = True
self.apply_pending = False
text = self.text_buffer.get_slice(
self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False)
if text != self.marked_up_text:
self.parent_conn.send(text)
else:
self.do_apply(text)
else:
self.apply_pending = True
def parse(self, child_conn):
"""Parses markup in a worker process."""
while True:
while True:
try:
text = child_conn.recv()
if not child_conn.poll():
break
except EOFError:
child_conn.close()
return
# List of tuples in the form (tag_name, tag_args, tag_start, tag_end).
result = []
# Find:
# - "_italic_" (italic)
# - "**bold**" (bold)
# - "***bolditalic***" (bold/italic)
# - "~~strikethrough~~" (strikethrough)
# - "`code`" (colorize)
# - "$math$" (colorize)
# - "---" table (wrap/pixels)
regexps = (
2019-12-22 15:06:35 +00:00
(ITALIC_ASTERISK, self.TAG_NAME_ITALIC),
(ITALIC_UNDERSCORE, self.TAG_NAME_ITALIC),
(BOLD, self.TAG_NAME_BOLD),
(BOLD_ITALIC, self.TAG_NAME_BOLD_ITALIC),
(STRIKETHROUGH, self.TAG_NAME_STRIKETHROUGH),
(CODE, self.TAG_NAME_CODE_TEXT),
(MATH, self.TAG_NAME_CODE_TEXT),
(TABLE, self.TAG_NAME_WRAP_NONE)
)
for regexp, tag_name in regexps:
matches = re.finditer(regexp, text)
for match in matches:
result.append((tag_name, (), match.start(), match.end()))
# Find:
# - "[description](url)" (gray out)
# - "![description](image_url)" (gray out)
regexps = (
(LINK, self.TAG_NAME_GRAY_TEXT),
(IMAGE, self.TAG_NAME_GRAY_TEXT)
)
for regexp, tag_name in regexps:
matches = re.finditer(regexp, text)
for match in matches:
result.append((tag_name, (), match.start(), match.start("text")))
result.append((tag_name, (), match.end("text"), match.end()))
# Find "<url>" links (gray out).
matches = re.finditer(LINK_ALT, text)
for match in matches:
result.append((
self.TAG_NAME_GRAY_TEXT, (), match.start("text"), match.end("text")))
# Find "---" horizontal rule (center).
matches = re.finditer(HORIZONTAL_RULE, text)
for match in matches:
result.append((
self.TAG_NAME_CENTER, (), match.start("symbols"), match.end("symbols")))
# Find "* list" (offset).
matches = re.finditer(LIST, text)
for match in matches:
# Lists use character+space (eg. "* ").
length = 2
nest = len(match.group("indent").replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
result.append((
self.TAG_NAME_MARGIN_INDENT,
(margin, indent),
match.start("content"),
match.end("content")
))
# Find "1. ordered list" (offset).
matches = re.finditer(ORDERED_LIST, text)
for match in matches:
# Ordered lists use numbers/letters+dot/parens+space (eg. "123. ").
length = len(match.group("prefix")) + 1
nest = len(match.group("indent").replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
result.append((
self.TAG_NAME_MARGIN_INDENT,
(margin, indent),
match.start("content"),
match.end("content")
))
# Find "> blockquote" (offset).
matches = re.finditer(BLOCK_QUOTE, text)
for match in matches:
result.append((self.TAG_NAME_MARGIN_INDENT, (2, -2), match.start(), match.end()))
# Find "# Header" (offset+bold).
matches = re.finditer(HEADER, text)
for match in matches:
margin = -len(match.group("level")) - 1
result.append((
self.TAG_NAME_MARGIN_INDENT, (margin, 0), match.start(), match.end()))
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
# Find "=======" header underline (bold).
matches = re.finditer(HEADER_UNDER, text)
for match in matches:
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
# Find "```" code block tag (offset + colorize paragraph).
matches = re.finditer(markup_regex.CODE_BLOCK, text)
for match in matches:
result.append((
self.TAG_NAME_CODE_BLOCK, (), match.start("block"), match.end("block")))
# Send parsed data back.
child_conn.send((text, result))
def on_parsed(self, _source, _condition):
"""Reads the parsing result from the pipe and triggers any pending apply."""
self.parsing = False
if self.apply_pending:
self.apply() # self.apply clears the apply pending flag.
try:
if self.parent_conn.poll():
self.do_apply(*self.parent_conn.recv())
return True
except EOFError:
return False
def do_apply(self, original_text, result=[]):
"""Applies the result of parsing if the current text matches the original text."""
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
buffer = self.text_buffer
start = buffer.get_start_iter()
end = buffer.get_end_iter()
text = self.text_buffer.get_slice(start, end, False)
# Apply markup tags.
if text == original_text and text != self.marked_up_text:
buffer.remove_tag(self.tag_italic, start, end)
buffer.remove_tag(self.tag_bold, start, end)
buffer.remove_tag(self.tag_bold_italic, start, end)
buffer.remove_tag(self.tag_strikethrough, start, end)
buffer.remove_tag(self.tag_center, start, end)
buffer.remove_tag(self.tag_plain_text, start, end)
buffer.remove_tag(self.tag_gray_text, start, end)
buffer.remove_tag(self.tag_code_text, start, end)
buffer.remove_tag(self.tag_code_block, start, end)
buffer.remove_tag(self.tag_wrap_none, start, end)
for tag in self.tags_margins_indents.values():
buffer.remove_tag(tag, start, end)
for tag_name, tag_args, tag_start, tag_end in result:
buffer.apply_tag(
self.tags_markup[tag_name](tag_args),
buffer.get_iter_at_offset(tag_start),
buffer.get_iter_at_offset(tag_end))
# Apply focus mode tag (grey out before/after current sentence).
buffer.remove_tag(self.tag_unfocused_text, start, end)
if self.text_view.focus_mode:
cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
start_sentence = cursor_iter.copy()
start_sentence.backward_sentence_start()
end_sentence = cursor_iter.copy()
end_sentence.forward_sentence_end()
buffer.apply_tag(self.tag_unfocused_text, start, start_sentence)
buffer.apply_tag(self.tag_unfocused_text, end_sentence, end)
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
# Margin and indent are cumulative. They differ in two ways:
# * Margin is always in the beginning, which means it effectively only affects the first line
# of multi-line text. Indent is applied to every line.
# * Margin level can be negative, as a baseline margin exists from which it can be subtracted.
# Indent is always positive, or 0.
def get_margin_indent_tag(self, margin_level, indent_level):
level = (margin_level, indent_level)
if level not in self.tags_margins_indents:
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
margin, indent = self.get_margin_indent(margin_level, indent_level)
tag = self.text_buffer.create_tag(
"margin_indent_{}_{}".format(margin_level, indent_level),
left_margin=margin, indent=indent)
self.tags_margins_indents[level] = tag
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
return tag
else:
return self.tags_margins_indents[level]
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
if baseline_margin is None:
baseline_margin = self.text_view.props.left_margin
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
if char_width is None:
char_width = helpers.get_char_width(self.text_view)
margin = max(baseline_margin + char_width * margin_level, 0)
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
indent = char_width * indent_level
return margin, indent
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
def update_margins_indents(self):
baseline_margin = self.text_view.props.left_margin
char_width = helpers.get_char_width(self.text_view)
# Bail out if neither the baseline margin nor character width change
if baseline_margin == self.baseline_margin and char_width == self.char_width:
return
self.baseline_margin = baseline_margin
self.char_width = char_width
# Adjust tab size
tab_array = Pango.TabArray.new(1, True)
Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
self.text_view.set_tabs(tab_array)
# Adjust margins and indents
for level, tag in self.tags_margins_indents.items():
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
tag.set_properties(left_margin=margin, indent=indent)
def stop(self, *_):
self.parent_conn.close()