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>'
|
||||
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user