mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on snippets
This commit is contained in:
parent
ca5e5f4d14
commit
fee978759a
@ -7,37 +7,46 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import re, copy
|
import re, copy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, namedtuple
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from PyQt5.Qt import QTextCursor
|
from PyQt5.Qt import QTextCursor, Qt, QObject
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
from calibre.gui2.tweak_book.editor import all_text_syntaxes
|
||||||
|
from calibre.gui2.tweak_book.editor.smarts.utils import get_text_before_cursor
|
||||||
from calibre.utils.config import JSONConfig
|
from calibre.utils.config import JSONConfig
|
||||||
from calibre.utils.icu import string_length
|
from calibre.utils.icu import string_length
|
||||||
|
|
||||||
|
SnipKey = namedtuple('SnipKey', 'trigger syntaxes')
|
||||||
|
def snip_key(trigger, *syntaxes):
|
||||||
|
if '*' in syntaxes:
|
||||||
|
syntaxes = all_text_syntaxes
|
||||||
|
return SnipKey(trigger, frozenset(*syntaxes))
|
||||||
|
|
||||||
builtin_snippets = { # {{{
|
builtin_snippets = { # {{{
|
||||||
'<<' : {
|
snip_key('<<', 'html', 'xml'): {
|
||||||
'description': _('Insert a HTML tag'),
|
'description': _('Insert a tag'),
|
||||||
'template': '<$1>${2*}</$1>',
|
'template': '<$1>${2*}</$1>',
|
||||||
},
|
},
|
||||||
|
|
||||||
'<>' : {
|
snip_key('<>', 'html', 'xml'): {
|
||||||
'description': _('Insert a self closing HTML tag'),
|
'description': _('Insert a self closing tag'),
|
||||||
'template': '<$1/>$2',
|
'template': '<$1/>$2',
|
||||||
},
|
},
|
||||||
|
|
||||||
'<a' : {
|
snip_key('<a', 'html'): {
|
||||||
'description': _('Insert a HTML link'),
|
'description': _('Insert a HTML link'),
|
||||||
'template': '<a href="$1">${2*}</a>',
|
'template': '<a href="$1">${2*}</a>',
|
||||||
},
|
},
|
||||||
|
|
||||||
'<i' : {
|
snip_key('<i', 'html'): {
|
||||||
'description': _('Insert a HTML image'),
|
'description': _('Insert a HTML image'),
|
||||||
'template': '<img src="$1" alt="${2*}" />$3',
|
'template': '<img src="$1" alt="${2*}" />$3',
|
||||||
},
|
},
|
||||||
|
|
||||||
'<c' : {
|
snip_key('<c', 'html'): {
|
||||||
'description': _('Insert a HTML tag with a class'),
|
'description': _('Insert a HTML tag with a class'),
|
||||||
'template': '<$1 class="$2">${3*}</$1>',
|
'template': '<$1 class="$2">${3*}</$1>',
|
||||||
},
|
},
|
||||||
@ -65,6 +74,8 @@ class TabStop(unicode):
|
|||||||
num, default = raw[2:-1].partition(':')[0::2]
|
num, default = raw[2:-1].partition(':')[0::2]
|
||||||
# Look for tab stops defined in the default text
|
# Look for tab stops defined in the default text
|
||||||
uraw, child_stops = parse_template(unescape(default), start_offset=start_offset, is_toplevel=False, grouped=False)
|
uraw, child_stops = parse_template(unescape(default), start_offset=start_offset, is_toplevel=False, grouped=False)
|
||||||
|
for c in child_stops:
|
||||||
|
c.parent = self
|
||||||
tab_stops.extend(child_stops)
|
tab_stops.extend(child_stops)
|
||||||
self = unicode.__new__(self, uraw)
|
self = unicode.__new__(self, uraw)
|
||||||
if num.endswith('*'):
|
if num.endswith('*'):
|
||||||
@ -80,6 +91,7 @@ class TabStop(unicode):
|
|||||||
self.start = start_offset
|
self.start = start_offset
|
||||||
self.is_toplevel = is_toplevel
|
self.is_toplevel = is_toplevel
|
||||||
self.is_mirror = False
|
self.is_mirror = False
|
||||||
|
self.parent = None
|
||||||
tab_stops.append(self)
|
tab_stops.append(self)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -117,68 +129,173 @@ def parse_template(template, start_offset=0, is_toplevel=True, grouped=True):
|
|||||||
_snippets = None
|
_snippets = None
|
||||||
user_snippets = JSONConfig('editor_snippets')
|
user_snippets = JSONConfig('editor_snippets')
|
||||||
|
|
||||||
def snippets():
|
def snippets(refresh=False):
|
||||||
global _snippets
|
global _snippets
|
||||||
if _snippets is None:
|
if _snippets is None or refresh:
|
||||||
_snippets = copy.deepcopy(builtin_snippets)
|
_snippets = copy.deepcopy(builtin_snippets)
|
||||||
us = copy.deepcopy(user_snippets.copy())
|
for snip in user_snippets.get('snippets', []):
|
||||||
_snippets.update(us)
|
if snip['trigger'] and isinstance(snip['trigger'], type('')):
|
||||||
|
key = snip_key(snip['trigger'], *snip['syntaxes'])
|
||||||
|
_snippets[key] = {'template':snip['template'], 'description':snip['description']}
|
||||||
return _snippets
|
return _snippets
|
||||||
|
|
||||||
class TabStopCursor(QTextCursor):
|
class EditorTabStop(object):
|
||||||
|
|
||||||
def __init__(self, other, tab_stops):
|
def __init__(self, other, tab_stops):
|
||||||
QTextCursor.__init__(self, other)
|
self.left = QTextCursor(other)
|
||||||
|
self.right = QTextCursor(other)
|
||||||
tab_stop = tab_stops[0]
|
tab_stop = tab_stops[0]
|
||||||
self.num = tab_stop.num
|
self.num = tab_stop.num
|
||||||
self.is_mirror = tab_stop.is_mirror
|
self.is_mirror = tab_stop.is_mirror
|
||||||
self.is_toplevel = tab_stop.is_toplevel
|
self.is_toplevel = tab_stop.is_toplevel
|
||||||
self.takes_selection = tab_stop.takes_selection
|
self.takes_selection = tab_stop.takes_selection
|
||||||
self.visited = False
|
self.left.setPosition(other.anchor() + tab_stop.start)
|
||||||
self.setPosition(other.anchor() + tab_stop.start)
|
|
||||||
l = string_length(tab_stop)
|
l = string_length(tab_stop)
|
||||||
if l > 0:
|
self.right.setPosition(self.left.position() + l)
|
||||||
self.setPosition(self.position() + l, self.KeepAnchor)
|
self.mirrors = tuple(EditorTabStop(other, [ts]) for ts in tab_stops[1:])
|
||||||
self.mirrors = []
|
|
||||||
for ts in tab_stops[1:]:
|
|
||||||
m = QTextCursor(other)
|
|
||||||
m.setPosition(other.anchor() + ts.start)
|
|
||||||
l = string_length(ts)
|
|
||||||
if l > 0:
|
|
||||||
m.setPosition(m.position() + l, m.KeepAnchor)
|
|
||||||
self.mirrors.append(m)
|
|
||||||
|
|
||||||
class CursorCollection(list):
|
def apply_selected_text(self, text):
|
||||||
|
if self.takes_selection:
|
||||||
|
self.text = text
|
||||||
|
for m in self.mirrors:
|
||||||
|
m.text = text
|
||||||
|
|
||||||
def __new__(self, cursors):
|
@dynamic_property
|
||||||
self = list.__new__(self, cursors)
|
def text(self):
|
||||||
|
def fget(self):
|
||||||
|
from calibre.gui2.tweak_book.editor.text import selected_text_from_cursor
|
||||||
|
c = QTextCursor(self.left)
|
||||||
|
c.setPosition(self.right.position(), c.KeepAnchor)
|
||||||
|
return selected_text_from_cursor(c)
|
||||||
|
def fset(self, text):
|
||||||
|
c = QTextCursor(self.left)
|
||||||
|
c.setPosition(self.right.position(), c.KeepAnchor)
|
||||||
|
c.insertText(text)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def set_editor_cursor(self, editor):
|
||||||
|
c = editor.textCursor()
|
||||||
|
c.setPosition(self.left.position())
|
||||||
|
c.setPosition(self.right.position(), c.KeepAnchor)
|
||||||
|
editor.setTextCursor(c)
|
||||||
|
|
||||||
|
class Template(list):
|
||||||
|
|
||||||
|
def __new__(self, tab_stops):
|
||||||
|
self = list.__new__(self, tab_stops)
|
||||||
self.left_most_cursor = self.right_most_cursor = None
|
self.left_most_cursor = self.right_most_cursor = None
|
||||||
for c in self:
|
for c in self:
|
||||||
if self.left_most_cursor is None or self.left_most_cursor.anchor() > c.anchor():
|
if self.left_most_cursor is None or self.left_most_cursor.position() > c.left.position():
|
||||||
self.left_most_cursor = c
|
self.left_most_cursor = c.left
|
||||||
if self.right_most_cursor is None or self.right_most_cursor.position() <= c.position():
|
if self.right_most_cursor is None or self.right_most_cursor.position() <= c.right.position():
|
||||||
self.right_most_cursor = c
|
self.right_most_cursor = c.right
|
||||||
|
self.has_tab_stops = self.left_most_cursor is not None
|
||||||
|
self.active_tab_stop = None
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def expand_template(editor, template_name, template, selected_text=''):
|
def contains_cursor(self, cursor):
|
||||||
|
if not self.has_tab_stops:
|
||||||
|
return False
|
||||||
|
pos = cursor.position()
|
||||||
|
if self.left_most_cursor.position() <= pos <= self.right_most_cursor.position():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def jump_to_next(self, editor):
|
||||||
|
if self.active_tab_stop is None:
|
||||||
|
self.active_tab_stop = ts = self.find_closest_tab_stop(editor.textCursor().position())
|
||||||
|
if ts is not None:
|
||||||
|
ts.set_editor_cursor(editor)
|
||||||
|
return ts
|
||||||
|
ts = self.active_tab_stop
|
||||||
|
for m in ts.mirrors:
|
||||||
|
m.text = ts.text
|
||||||
|
for x in self:
|
||||||
|
if x.num > ts.num:
|
||||||
|
self.active_tab_stop = x
|
||||||
|
x.set_editor_cursor(editor)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def distance_to_position(self, cursor, position):
|
||||||
|
return min(abs(cursor.position() - position), abs(cursor.anchor() - position))
|
||||||
|
|
||||||
|
def find_closest_tab_stop(self, position):
|
||||||
|
ans = dist = None
|
||||||
|
for c in self:
|
||||||
|
x = min(self.distance_to_position(c.left, position), self.distance_to_position(c.right, position))
|
||||||
|
if ans is None or x < dist:
|
||||||
|
dist, ans = x, c
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def expand_template(editor, trigger, template, selected_text=''):
|
||||||
c = editor.textCursor()
|
c = editor.textCursor()
|
||||||
c.setPosition(c.position())
|
c.setPosition(c.position())
|
||||||
right = c.position()
|
right = c.position()
|
||||||
left = right - string_length(template_name)
|
left = right - string_length(trigger)
|
||||||
text, tab_stops = parse_template(template)
|
text, tab_stops = parse_template(template)
|
||||||
cursors = []
|
|
||||||
c.setPosition(left), c.setPosition(right, c.KeepAnchor), c.insertText(text)
|
c.setPosition(left), c.setPosition(right, c.KeepAnchor), c.insertText(text)
|
||||||
for i, ts in enumerate(tab_stops.itervalues()):
|
editor_tab_stops = [EditorTabStop(c, ts) for ts in tab_stops]
|
||||||
tsc = TabStopCursor(c, ts)
|
|
||||||
cursors.append(tsc)
|
|
||||||
|
|
||||||
if selected_text:
|
if selected_text:
|
||||||
for tsc in cursors:
|
for ts in editor_tab_stops:
|
||||||
pos = min(tsc.anchor(), tsc.position())
|
ts.apply_selected_text(selected_text)
|
||||||
tsc.insertText(selected_text)
|
tl = Template(editor_tab_stops)
|
||||||
apos = tsc.position()
|
if tl.has_tab_stops:
|
||||||
tsc.setPosition(pos), tsc.setPosition(apos, tsc.KeepAnchor)
|
tl.active_tab_stop = ts = editor_tab_stops[0]
|
||||||
active_cursor = (cursors or [c])[0]
|
ts.set_editor_cursor(editor)
|
||||||
active_cursor.visited = True
|
else:
|
||||||
editor.setTextCursor(active_cursor)
|
editor.setTextCursor(c)
|
||||||
return CursorCollection(cursors)
|
return tl
|
||||||
|
|
||||||
|
def find_matching_snip(text, syntax):
|
||||||
|
for key, snip in snippets().iteritems():
|
||||||
|
if text.endswith(key.trigger) and syntax in key.syntaxes:
|
||||||
|
return snip, key.trigger
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
class SnippetManager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, editor):
|
||||||
|
QObject.__init__(self, editor)
|
||||||
|
self.active_templates = []
|
||||||
|
self.last_selected_text = ''
|
||||||
|
|
||||||
|
def get_active_template(self, cursor):
|
||||||
|
remove = []
|
||||||
|
at = None
|
||||||
|
pos = cursor.position()
|
||||||
|
for template in self.active_templates:
|
||||||
|
if at is None and template.contains_cursor(cursor):
|
||||||
|
at = template
|
||||||
|
elif pos > template.right_most_cursor.position() or pos < template.left_most_cursor.position():
|
||||||
|
remove.append(template)
|
||||||
|
for template in remove:
|
||||||
|
self.active_templates.remove(template)
|
||||||
|
return at
|
||||||
|
|
||||||
|
def handle_keypress(self, ev):
|
||||||
|
editor = self.parent()
|
||||||
|
if ev.key() == Qt.Key_Tab and ev.modifiers() & Qt.CTRL:
|
||||||
|
at = self.get_active_template(editor.cursor())
|
||||||
|
if at is not None:
|
||||||
|
if at.jump_to_next(editor) is None:
|
||||||
|
self.active_templates.remove(at)
|
||||||
|
ev.accept()
|
||||||
|
return True
|
||||||
|
self.last_selected_text = editor.selected_text
|
||||||
|
if self.last_selected_text:
|
||||||
|
editor.textCursor().insertText('')
|
||||||
|
ev.accept()
|
||||||
|
return True
|
||||||
|
c, text = get_text_before_cursor(editor)
|
||||||
|
snip, trigger = find_matching_snip(text, editor.syntax)
|
||||||
|
if snip is None:
|
||||||
|
error_dialog(self.parent(), _('No snippet found'), _(
|
||||||
|
'No matching snippet was found'), show=True)
|
||||||
|
return False
|
||||||
|
template = expand_template(editor, trigger, snip['template'], self.last_selected_text)
|
||||||
|
if template.has_tab_stops:
|
||||||
|
self.active_templates.append(template)
|
||||||
|
ev.accept()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user