More work on snippets

This commit is contained in:
Kovid Goyal 2015-01-06 13:56:41 +05:30
parent ca5e5f4d14
commit fee978759a

View File

@ -7,37 +7,46 @@ __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import re, copy
from collections import OrderedDict
from collections import OrderedDict, namedtuple
from itertools import groupby
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.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 = { # {{{
'<<' : {
'description': _('Insert a HTML tag'),
snip_key('<<', 'html', 'xml'): {
'description': _('Insert a tag'),
'template': '<$1>${2*}</$1>',
},
'<>' : {
'description': _('Insert a self closing HTML tag'),
snip_key('<>', 'html', 'xml'): {
'description': _('Insert a self closing tag'),
'template': '<$1/>$2',
},
'<a' : {
snip_key('<a', 'html'): {
'description': _('Insert a HTML link'),
'template': '<a href="$1">${2*}</a>',
},
'<i' : {
snip_key('<i', 'html'): {
'description': _('Insert a HTML image'),
'template': '<img src="$1" alt="${2*}" />$3',
},
'<c' : {
snip_key('<c', 'html'): {
'description': _('Insert a HTML tag with a class'),
'template': '<$1 class="$2">${3*}</$1>',
},
@ -65,6 +74,8 @@ class TabStop(unicode):
num, default = raw[2:-1].partition(':')[0::2]
# 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)
for c in child_stops:
c.parent = self
tab_stops.extend(child_stops)
self = unicode.__new__(self, uraw)
if num.endswith('*'):
@ -80,6 +91,7 @@ class TabStop(unicode):
self.start = start_offset
self.is_toplevel = is_toplevel
self.is_mirror = False
self.parent = None
tab_stops.append(self)
return self
@ -117,68 +129,173 @@ def parse_template(template, start_offset=0, is_toplevel=True, grouped=True):
_snippets = None
user_snippets = JSONConfig('editor_snippets')
def snippets():
def snippets(refresh=False):
global _snippets
if _snippets is None:
if _snippets is None or refresh:
_snippets = copy.deepcopy(builtin_snippets)
us = copy.deepcopy(user_snippets.copy())
_snippets.update(us)
for snip in user_snippets.get('snippets', []):
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
class TabStopCursor(QTextCursor):
class EditorTabStop(object):
def __init__(self, other, tab_stops):
QTextCursor.__init__(self, other)
self.left = QTextCursor(other)
self.right = QTextCursor(other)
tab_stop = tab_stops[0]
self.num = tab_stop.num
self.is_mirror = tab_stop.is_mirror
self.is_toplevel = tab_stop.is_toplevel
self.takes_selection = tab_stop.takes_selection
self.visited = False
self.setPosition(other.anchor() + tab_stop.start)
self.left.setPosition(other.anchor() + tab_stop.start)
l = string_length(tab_stop)
if l > 0:
self.setPosition(self.position() + l, self.KeepAnchor)
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)
self.right.setPosition(self.left.position() + l)
self.mirrors = tuple(EditorTabStop(other, [ts]) for ts in tab_stops[1:])
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):
self = list.__new__(self, cursors)
@dynamic_property
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
for c in self:
if self.left_most_cursor is None or self.left_most_cursor.anchor() > c.anchor():
self.left_most_cursor = c
if self.right_most_cursor is None or self.right_most_cursor.position() <= c.position():
self.right_most_cursor = c
if self.left_most_cursor is None or self.left_most_cursor.position() > c.left.position():
self.left_most_cursor = c.left
if self.right_most_cursor is None or self.right_most_cursor.position() <= c.right.position():
self.right_most_cursor = c.right
self.has_tab_stops = self.left_most_cursor is not None
self.active_tab_stop = None
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.setPosition(c.position())
right = c.position()
left = right - string_length(template_name)
left = right - string_length(trigger)
text, tab_stops = parse_template(template)
cursors = []
c.setPosition(left), c.setPosition(right, c.KeepAnchor), c.insertText(text)
for i, ts in enumerate(tab_stops.itervalues()):
tsc = TabStopCursor(c, ts)
cursors.append(tsc)
editor_tab_stops = [EditorTabStop(c, ts) for ts in tab_stops]
if selected_text:
for tsc in cursors:
pos = min(tsc.anchor(), tsc.position())
tsc.insertText(selected_text)
apos = tsc.position()
tsc.setPosition(pos), tsc.setPosition(apos, tsc.KeepAnchor)
active_cursor = (cursors or [c])[0]
active_cursor.visited = True
editor.setTextCursor(active_cursor)
return CursorCollection(cursors)
for ts in editor_tab_stops:
ts.apply_selected_text(selected_text)
tl = Template(editor_tab_stops)
if tl.has_tab_stops:
tl.active_tab_stop = ts = editor_tab_stops[0]
ts.set_editor_cursor(editor)
else:
editor.setTextCursor(c)
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