1090 lines
44 KiB
Python
Executable File
1090 lines
44 KiB
Python
Executable File
# Pluma snippets plugin
|
|
# Copyright (C) 2005-2006 Jesse van den Kieboom <jesse@icecrew.nl>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY 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, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
import os
|
|
import re
|
|
|
|
import gtk
|
|
from gtk import gdk
|
|
import gio
|
|
import pluma
|
|
import gtksourceview2 as gsv
|
|
import gobject
|
|
|
|
from Library import Library
|
|
from Snippet import Snippet
|
|
from Placeholder import *
|
|
import Completion
|
|
|
|
class DynamicSnippet(dict):
|
|
def __init__(self, text):
|
|
self['text'] = text
|
|
self.valid = True
|
|
|
|
class Document:
|
|
TAB_KEY_VAL = (gtk.keysyms.Tab, \
|
|
gtk.keysyms.ISO_Left_Tab)
|
|
SPACE_KEY_VAL = (gtk.keysyms.space,)
|
|
|
|
def __init__(self, instance, view):
|
|
self.view = None
|
|
self.instance = instance
|
|
|
|
self.placeholders = []
|
|
self.active_snippets = []
|
|
self.active_placeholder = None
|
|
self.signal_ids = {}
|
|
|
|
self.ordered_placeholders = []
|
|
self.update_placeholders = []
|
|
self.jump_placeholders = []
|
|
self.language_id = 0
|
|
self.timeout_update_id = 0
|
|
|
|
self.provider = Completion.Provider(_('Snippets'), self.language_id, self.on_proposal_activated)
|
|
self.defaults_provider = Completion.Defaults(self.on_default_activated)
|
|
|
|
# Always have a reference to the global snippets
|
|
Library().ref(None)
|
|
self.set_view(view)
|
|
|
|
# Stop controlling the view. Remove all active snippets, remove references
|
|
# to the view and the plugin instance, disconnect all signal handlers
|
|
def stop(self):
|
|
if self.timeout_update_id != 0:
|
|
gobject.source_remove(self.timeout_update_id)
|
|
self.timeout_update_id = 0
|
|
del self.update_placeholders[:]
|
|
del self.jump_placeholders[:]
|
|
|
|
# Always release the reference to the global snippets
|
|
Library().unref(None)
|
|
self.set_view(None)
|
|
self.instance = None
|
|
self.active_placeholder = None
|
|
|
|
def disconnect_signal(self, obj, signal):
|
|
if (obj, signal) in self.signal_ids:
|
|
obj.disconnect(self.signal_ids[(obj, signal)])
|
|
del self.signal_ids[(obj, signal)]
|
|
|
|
def connect_signal(self, obj, signal, cb):
|
|
self.disconnect_signal(obj, signal)
|
|
self.signal_ids[(obj, signal)] = obj.connect(signal, cb)
|
|
|
|
def connect_signal_after(self, obj, signal, cb):
|
|
self.disconnect_signal(obj, signal)
|
|
self.signal_ids[(obj, signal)] = obj.connect_after(signal, cb)
|
|
|
|
# Set the view to be controlled. Installs signal handlers and sets current
|
|
# language. If there is already a view set this function will first remove
|
|
# all currently active snippets and disconnect all current signals. So
|
|
# self.set_view(None) will effectively remove all the control from the
|
|
# current view
|
|
def _set_view(self, view):
|
|
if self.view:
|
|
buf = self.view.get_buffer()
|
|
|
|
# Remove signals
|
|
signals = {self.view: ('key-press-event', 'destroy',
|
|
'notify::editable', 'drag-data-received', 'expose-event'),
|
|
buf: ('notify::language', 'changed', 'cursor-moved', 'insert-text'),
|
|
self.view.get_completion(): ('hide',)}
|
|
|
|
for obj, sig in signals.items():
|
|
for s in sig:
|
|
self.disconnect_signal(obj, s)
|
|
|
|
# Remove all active snippets
|
|
for snippet in list(self.active_snippets):
|
|
self.deactivate_snippet(snippet, True)
|
|
|
|
completion = self.view.get_completion()
|
|
completion.remove_provider(self.provider)
|
|
completion.remove_provider(self.defaults_provider)
|
|
|
|
self.view = view
|
|
|
|
if view != None:
|
|
buf = view.get_buffer()
|
|
|
|
self.connect_signal(view, 'destroy', self.on_view_destroy)
|
|
|
|
if view.get_editable():
|
|
self.connect_signal(view, 'key-press-event', self.on_view_key_press)
|
|
|
|
self.connect_signal(buf, 'notify::language', self.on_notify_language)
|
|
self.connect_signal(view, 'notify::editable', self.on_notify_editable)
|
|
self.connect_signal(view, 'drag-data-received', self.on_drag_data_received)
|
|
self.connect_signal_after(view, 'expose-event', self.on_expose_event)
|
|
|
|
self.update_language()
|
|
|
|
completion = view.get_completion()
|
|
completion.add_provider(self.provider)
|
|
|
|
completion.add_provider(self.defaults_provider)
|
|
|
|
self.connect_signal(completion, 'hide', self.on_completion_hide)
|
|
elif self.language_id != 0:
|
|
langid = self.language_id
|
|
|
|
self.language_id = None;
|
|
self.provider.language_id = self.language_id
|
|
|
|
if self.instance:
|
|
self.instance.language_changed(self)
|
|
|
|
Library().unref(langid)
|
|
|
|
def set_view(self, view):
|
|
if view == self.view:
|
|
return
|
|
|
|
self._set_view(view)
|
|
|
|
# Call this whenever the language in the view changes. This makes sure that
|
|
# the correct language is used when finding snippets
|
|
def update_language(self):
|
|
lang = self.view.get_buffer().get_language()
|
|
|
|
if lang == None and self.language_id == None:
|
|
return
|
|
elif lang and lang.get_id() == self.language_id:
|
|
return
|
|
|
|
langid = self.language_id
|
|
|
|
if lang:
|
|
self.language_id = lang.get_id()
|
|
else:
|
|
self.language_id = None
|
|
|
|
if self.instance:
|
|
self.instance.language_changed(self)
|
|
|
|
if langid != 0:
|
|
Library().unref(langid)
|
|
|
|
Library().ref(self.language_id)
|
|
self.provider.language_id = self.language_id
|
|
|
|
def accelerator_activate(self, keyval, mod):
|
|
if not self.view or not self.view.get_editable():
|
|
return False
|
|
|
|
accelerator = gtk.accelerator_name(keyval, mod)
|
|
snippets = Library().from_accelerator(accelerator, \
|
|
self.language_id)
|
|
|
|
snippets_debug('Accel!')
|
|
|
|
if len(snippets) == 0:
|
|
return False
|
|
elif len(snippets) == 1:
|
|
self.apply_snippet(snippets[0])
|
|
else:
|
|
# Do the fancy completion dialog
|
|
self.provider.set_proposals(snippets)
|
|
self.view.show_completion((self,))
|
|
|
|
return True
|
|
|
|
def first_snippet_inserted(self):
|
|
buf = self.view.get_buffer()
|
|
|
|
self.connect_signal(buf, 'changed', self.on_buffer_changed)
|
|
self.connect_signal(buf, 'cursor-moved', self.on_buffer_cursor_moved)
|
|
self.connect_signal_after(buf, 'insert-text', self.on_buffer_insert_text)
|
|
|
|
def last_snippet_removed(self):
|
|
buf = self.view.get_buffer()
|
|
self.disconnect_signal(buf, 'changed')
|
|
self.disconnect_signal(buf, 'cursor-moved')
|
|
self.disconnect_signal(buf, 'insert-text')
|
|
|
|
def current_placeholder(self):
|
|
buf = self.view.get_buffer()
|
|
|
|
piter = buf.get_iter_at_mark(buf.get_insert())
|
|
found = []
|
|
|
|
for placeholder in self.placeholders:
|
|
begin = placeholder.begin_iter()
|
|
end = placeholder.end_iter()
|
|
|
|
if piter.compare(begin) >= 0 and piter.compare(end) <= 0:
|
|
found.append(placeholder)
|
|
|
|
if self.active_placeholder in found:
|
|
return self.active_placeholder
|
|
elif len(found) > 0:
|
|
return found[0]
|
|
else:
|
|
return None
|
|
|
|
def advance_placeholder(self, direction):
|
|
# Returns (CurrentPlaceholder, NextPlaceholder), depending on direction
|
|
buf = self.view.get_buffer()
|
|
|
|
piter = buf.get_iter_at_mark(buf.get_insert())
|
|
found = current = next = None
|
|
length = len(self.placeholders)
|
|
|
|
placeholders = list(self.placeholders)
|
|
|
|
if self.active_placeholder:
|
|
begin = self.active_placeholder.begin_iter()
|
|
end = self.active_placeholder.end_iter()
|
|
|
|
if piter.compare(begin) >= 0 and piter.compare(end) <= 0:
|
|
current = self.active_placeholder
|
|
currentIndex = placeholders.index(self.active_placeholder)
|
|
|
|
if direction == 1:
|
|
# w = piter, x = begin, y = end, z = found
|
|
nearest = lambda w, x, y, z: (w.compare(x) <= 0 and (not z or \
|
|
x.compare(z.begin_iter()) < 0))
|
|
indexer = lambda x: x < length - 1
|
|
else:
|
|
# w = piter, x = begin, y = end, z = prev
|
|
nearest = lambda w, x, y, z: (w.compare(x) >= 0 and (not z or \
|
|
x.compare(z.begin_iter()) >= 0))
|
|
indexer = lambda x: x > 0
|
|
|
|
for index in range(0, length):
|
|
placeholder = placeholders[index]
|
|
begin = placeholder.begin_iter()
|
|
end = placeholder.end_iter()
|
|
|
|
# Find the nearest placeholder
|
|
if nearest(piter, begin, end, found):
|
|
foundIndex = index
|
|
found = placeholder
|
|
|
|
# Find the current placeholder
|
|
if piter.compare(begin) >= 0 and \
|
|
piter.compare(end) <= 0 and \
|
|
current == None:
|
|
currentIndex = index
|
|
current = placeholder
|
|
|
|
if current and current != found and \
|
|
(current.begin_iter().compare(found.begin_iter()) == 0 or \
|
|
current.end_iter().compare(found.begin_iter()) == 0) and \
|
|
self.active_placeholder and \
|
|
current.begin_iter().compare(self.active_placeholder.begin_iter()) == 0:
|
|
# if current and found are at the same place, then
|
|
# resolve the 'hugging' problem
|
|
current = self.active_placeholder
|
|
currentIndex = placeholders.index(current)
|
|
|
|
if current:
|
|
if indexer(currentIndex):
|
|
next = placeholders[currentIndex + direction]
|
|
elif found:
|
|
next = found
|
|
elif length > 0:
|
|
next = self.placeholders[0]
|
|
|
|
return current, next
|
|
|
|
def next_placeholder(self):
|
|
return self.advance_placeholder(1)
|
|
|
|
def previous_placeholder(self):
|
|
return self.advance_placeholder(-1)
|
|
|
|
def cursor_on_screen(self):
|
|
buf = self.view.get_buffer()
|
|
self.view.scroll_mark_onscreen(buf.get_insert())
|
|
|
|
def set_active_placeholder(self, placeholder):
|
|
self.active_placeholder = placeholder
|
|
|
|
def goto_placeholder(self, current, next):
|
|
last = None
|
|
|
|
if current:
|
|
# Signal this placeholder to end action
|
|
self.view.get_completion().hide()
|
|
current.leave()
|
|
|
|
if current.__class__ == PlaceholderEnd:
|
|
last = current
|
|
|
|
self.set_active_placeholder(next)
|
|
|
|
if next:
|
|
next.enter()
|
|
|
|
if next.__class__ == PlaceholderEnd:
|
|
last = next
|
|
elif len(next.defaults) > 1 and next.get_text() == next.default:
|
|
self.defaults_provider.set_defaults(next.defaults)
|
|
|
|
cm = self.view.get_completion()
|
|
cm.show([self.defaults_provider], cm.create_context())
|
|
|
|
if last:
|
|
# This is the end of the placeholder, remove the snippet etc
|
|
for snippet in list(self.active_snippets):
|
|
if snippet.placeholders[0] == last:
|
|
self.deactivate_snippet(snippet)
|
|
break
|
|
|
|
self.cursor_on_screen()
|
|
|
|
return next != None
|
|
|
|
def skip_to_next_placeholder(self):
|
|
(current, next) = self.next_placeholder()
|
|
return self.goto_placeholder(current, next)
|
|
|
|
def skip_to_previous_placeholder(self):
|
|
(current, prev) = self.previous_placeholder()
|
|
return self.goto_placeholder(current, prev)
|
|
|
|
def env_get_selected_text(self, buf):
|
|
bounds = buf.get_selection_bounds()
|
|
|
|
if bounds:
|
|
return buf.get_text(bounds[0], bounds[1])
|
|
else:
|
|
return ''
|
|
|
|
def env_get_current_word(self, buf):
|
|
start, end = buffer_word_boundary(buf)
|
|
|
|
return buf.get_text(start, end)
|
|
|
|
def env_get_current_line(self, buf):
|
|
start, end = buffer_line_boundary(buf)
|
|
|
|
return buf.get_text(start, end)
|
|
|
|
def env_get_current_line_number(self, buf):
|
|
start, end = buffer_line_boundary(buf)
|
|
|
|
return str(start.get_line() + 1)
|
|
|
|
def env_get_document_uri(self, buf):
|
|
location = buf.get_location()
|
|
|
|
if location:
|
|
return location.get_uri()
|
|
else:
|
|
return ''
|
|
|
|
def env_get_document_name(self, buf):
|
|
location = buf.get_location()
|
|
|
|
if location:
|
|
return location.get_basename()
|
|
else:
|
|
return ''
|
|
|
|
def env_get_document_scheme(self, buf):
|
|
location = buf.get_location()
|
|
|
|
if location:
|
|
return location.get_uri_scheme()
|
|
else:
|
|
return ''
|
|
|
|
def env_get_document_path(self, buf):
|
|
location = buf.get_location()
|
|
|
|
if location:
|
|
return location.get_path()
|
|
else:
|
|
return ''
|
|
|
|
def env_get_document_dir(self, buf):
|
|
location = buf.get_location()
|
|
|
|
if location:
|
|
return location.get_parent().get_path() or ''
|
|
else:
|
|
return ''
|
|
|
|
def env_get_document_type(self, buf):
|
|
typ = buf.get_mime_type()
|
|
|
|
if typ:
|
|
return typ
|
|
else:
|
|
return ''
|
|
|
|
def env_get_documents_uri(self, buf):
|
|
toplevel = self.view.get_toplevel()
|
|
|
|
if isinstance(toplevel, pluma.Window):
|
|
documents_uri = [doc.get_location().get_uri()
|
|
for doc in toplevel.get_documents()
|
|
if doc.get_location() is not None]
|
|
else:
|
|
documents_uri = []
|
|
|
|
return ' '.join(documents_uri)
|
|
|
|
def env_get_documents_path(self, buf):
|
|
toplevel = self.view.get_toplevel()
|
|
|
|
if isinstance(toplevel, pluma.Window):
|
|
documents_location = [doc.get_location()
|
|
for doc in toplevel.get_documents()
|
|
if doc.get_location() is not None]
|
|
|
|
documents_path = [location.get_path()
|
|
for location in documents_location
|
|
if pluma.utils.uri_has_file_scheme(location.get_uri())]
|
|
else:
|
|
documents_path = []
|
|
|
|
return ' '.join(documents_path)
|
|
|
|
def update_environment(self):
|
|
buf = self.view.get_buffer()
|
|
|
|
variables = {'PLUMA_SELECTED_TEXT': self.env_get_selected_text,
|
|
'PLUMA_CURRENT_WORD': self.env_get_current_word,
|
|
'PLUMA_CURRENT_LINE': self.env_get_current_line,
|
|
'PLUMA_CURRENT_LINE_NUMBER': self.env_get_current_line_number,
|
|
'PLUMA_CURRENT_DOCUMENT_URI': self.env_get_document_uri,
|
|
'PLUMA_CURRENT_DOCUMENT_NAME': self.env_get_document_name,
|
|
'PLUMA_CURRENT_DOCUMENT_SCHEME': self.env_get_document_scheme,
|
|
'PLUMA_CURRENT_DOCUMENT_PATH': self.env_get_document_path,
|
|
'PLUMA_CURRENT_DOCUMENT_DIR': self.env_get_document_dir,
|
|
'PLUMA_CURRENT_DOCUMENT_TYPE': self.env_get_document_type,
|
|
'PLUMA_DOCUMENTS_URI': self.env_get_documents_uri,
|
|
'PLUMA_DOCUMENTS_PATH': self.env_get_documents_path,
|
|
}
|
|
|
|
for var in variables:
|
|
os.environ[var] = variables[var](buf)
|
|
|
|
def uses_current_word(self, snippet):
|
|
matches = re.findall('(\\\\*)\\$PLUMA_CURRENT_WORD', snippet['text'])
|
|
|
|
for match in matches:
|
|
if len(match) % 2 == 0:
|
|
return True
|
|
|
|
return False
|
|
|
|
def uses_current_line(self, snippet):
|
|
matches = re.findall('(\\\\*)\\$PLUMA_CURRENT_LINE', snippet['text'])
|
|
|
|
for match in matches:
|
|
if len(match) % 2 == 0:
|
|
return True
|
|
|
|
return False
|
|
|
|
def apply_snippet(self, snippet, start = None, end = None):
|
|
if not snippet.valid:
|
|
return False
|
|
|
|
buf = self.view.get_buffer()
|
|
s = Snippet(snippet)
|
|
|
|
if not start:
|
|
start = buf.get_iter_at_mark(buf.get_insert())
|
|
|
|
if not end:
|
|
end = buf.get_iter_at_mark(buf.get_selection_bound())
|
|
|
|
if start.equal(end) and self.uses_current_word(s):
|
|
# There is no tab trigger and no selection and the snippet uses
|
|
# the current word. Set start and end to the word boundary so that
|
|
# it will be removed
|
|
start, end = buffer_word_boundary(buf)
|
|
elif start.equal(end) and self.uses_current_line(s):
|
|
# There is no tab trigger and no selection and the snippet uses
|
|
# the current line. Set start and end to the line boundary so that
|
|
# it will be removed
|
|
start, end = buffer_line_boundary(buf)
|
|
|
|
# Set environmental variables
|
|
self.update_environment()
|
|
|
|
# You know, we could be in an end placeholder
|
|
(current, next) = self.next_placeholder()
|
|
if current and current.__class__ == PlaceholderEnd:
|
|
self.goto_placeholder(current, None)
|
|
|
|
buf.begin_user_action()
|
|
|
|
# Remove the tag, selection or current word
|
|
buf.delete(start, end)
|
|
|
|
# Insert the snippet
|
|
holders = len(self.placeholders)
|
|
|
|
if len(self.active_snippets) == 0:
|
|
self.first_snippet_inserted()
|
|
|
|
sn = s.insert_into(self, start)
|
|
self.active_snippets.append(sn)
|
|
|
|
# Put cursor at first tab placeholder
|
|
keys = filter(lambda x: x > 0, sn.placeholders.keys())
|
|
|
|
if len(keys) == 0:
|
|
if 0 in sn.placeholders:
|
|
self.goto_placeholder(self.active_placeholder, sn.placeholders[0])
|
|
else:
|
|
buf.place_cursor(sn.begin_iter())
|
|
else:
|
|
self.goto_placeholder(self.active_placeholder, sn.placeholders[keys[0]])
|
|
|
|
if sn in self.active_snippets:
|
|
# Check if we can get end_iter in view without moving the
|
|
# current cursor position out of view
|
|
cur = buf.get_iter_at_mark(buf.get_insert())
|
|
last = sn.end_iter()
|
|
|
|
curloc = self.view.get_iter_location(cur)
|
|
lastloc = self.view.get_iter_location(last)
|
|
|
|
if (lastloc.y + lastloc.height) - curloc.y <= \
|
|
self.view.get_visible_rect().height:
|
|
self.view.scroll_mark_onscreen(sn.end_mark)
|
|
|
|
buf.end_user_action()
|
|
self.view.grab_focus()
|
|
|
|
return True
|
|
|
|
def get_tab_tag(self, buf, end = None):
|
|
if not end:
|
|
end = buf.get_iter_at_mark(buf.get_insert())
|
|
|
|
start = end.copy()
|
|
|
|
word = None
|
|
|
|
if start.backward_word_start():
|
|
# Check if we were at a word start ourselves
|
|
tmp = start.copy()
|
|
tmp.forward_word_end()
|
|
|
|
if tmp.equal(end):
|
|
word = buf.get_text(start, end)
|
|
else:
|
|
start = end.copy()
|
|
else:
|
|
start = end.copy()
|
|
|
|
if not word or word == '':
|
|
if start.backward_char():
|
|
word = start.get_char()
|
|
|
|
if word.isalnum() or word.isspace():
|
|
return (None, None, None)
|
|
else:
|
|
return (None, None, None)
|
|
|
|
return (word, start, end)
|
|
|
|
def parse_and_run_snippet(self, data, iter):
|
|
self.apply_snippet(DynamicSnippet(data), iter, iter)
|
|
|
|
def run_snippet_trigger(self, trigger, bounds):
|
|
if not self.view:
|
|
return False
|
|
|
|
snippets = Library().from_tag(trigger, self.language_id)
|
|
buf = self.view.get_buffer()
|
|
|
|
if snippets:
|
|
if len(snippets) == 1:
|
|
return self.apply_snippet(snippets[0], bounds[0], bounds[1])
|
|
else:
|
|
# Do the fancy completion dialog
|
|
self.provider.set_proposals(snippets)
|
|
cm = self.view.get_completion()
|
|
|
|
cm.show([self.provider], cm.create_context())
|
|
return True
|
|
|
|
return False
|
|
|
|
def run_snippet(self):
|
|
if not self.view:
|
|
return False
|
|
|
|
buf = self.view.get_buffer()
|
|
|
|
# get the word preceding the current insertion position
|
|
(word, start, end) = self.get_tab_tag(buf)
|
|
|
|
if not word:
|
|
return self.skip_to_next_placeholder()
|
|
|
|
if not self.run_snippet_trigger(word, (start, end)):
|
|
return self.skip_to_next_placeholder()
|
|
else:
|
|
return True
|
|
|
|
def deactivate_snippet(self, snippet, force = False):
|
|
buf = self.view.get_buffer()
|
|
remove = []
|
|
ordered_remove = []
|
|
|
|
for tabstop in snippet.placeholders:
|
|
if tabstop == -1:
|
|
placeholders = snippet.placeholders[-1]
|
|
else:
|
|
placeholders = [snippet.placeholders[tabstop]]
|
|
|
|
for placeholder in placeholders:
|
|
if placeholder in self.placeholders:
|
|
if placeholder in self.update_placeholders:
|
|
placeholder.update_contents()
|
|
|
|
self.update_placeholders.remove(placeholder)
|
|
elif placeholder in self.jump_placeholders:
|
|
placeholder[0].leave()
|
|
|
|
remove.append(placeholder)
|
|
elif placeholder in self.ordered_placeholders:
|
|
ordered_remove.append(placeholder)
|
|
|
|
for placeholder in remove:
|
|
if placeholder == self.active_placeholder:
|
|
self.active_placeholder = None
|
|
|
|
self.placeholders.remove(placeholder)
|
|
self.ordered_placeholders.remove(placeholder)
|
|
|
|
placeholder.remove(force)
|
|
|
|
for placeholder in ordered_remove:
|
|
self.ordered_placeholders.remove(placeholder)
|
|
placeholder.remove(force)
|
|
|
|
snippet.deactivate()
|
|
self.active_snippets.remove(snippet)
|
|
|
|
if len(self.active_snippets) == 0:
|
|
self.last_snippet_removed()
|
|
|
|
self.view.queue_draw()
|
|
|
|
def update_snippet_contents(self):
|
|
self.timeout_update_id = 0
|
|
|
|
for placeholder in self.update_placeholders:
|
|
placeholder.update_contents()
|
|
|
|
for placeholder in self.jump_placeholders:
|
|
self.goto_placeholder(placeholder[0], placeholder[1])
|
|
|
|
del self.update_placeholders[:]
|
|
del self.jump_placeholders[:]
|
|
|
|
return False
|
|
|
|
# Callbacks
|
|
def on_view_destroy(self, view):
|
|
self.stop()
|
|
return
|
|
|
|
def on_buffer_cursor_moved(self, buf):
|
|
piter = buf.get_iter_at_mark(buf.get_insert())
|
|
|
|
# Check for all snippets if the cursor is outside its scope
|
|
for snippet in list(self.active_snippets):
|
|
if snippet.begin_mark.get_deleted() or snippet.end_mark.get_deleted():
|
|
self.deactivate(snippet)
|
|
else:
|
|
begin = snippet.begin_iter()
|
|
end = snippet.end_iter()
|
|
|
|
if piter.compare(begin) < 0 or piter.compare(end) > 0:
|
|
# Oh no! Remove the snippet this instant!!
|
|
self.deactivate_snippet(snippet)
|
|
|
|
current = self.current_placeholder()
|
|
|
|
if current != self.active_placeholder:
|
|
self.jump_placeholders.append((self.active_placeholder, current))
|
|
|
|
if self.timeout_update_id == 0:
|
|
self.timeout_update_id = gobject.timeout_add(0,
|
|
self.update_snippet_contents)
|
|
|
|
def on_buffer_changed(self, buf):
|
|
current = self.current_placeholder()
|
|
|
|
if current:
|
|
if not current in self.update_placeholders:
|
|
self.update_placeholders.append(current)
|
|
|
|
if self.timeout_update_id == 0:
|
|
self.timeout_update_id = gobject.timeout_add(0, \
|
|
self.update_snippet_contents)
|
|
|
|
def on_buffer_insert_text(self, buf, piter, text, length):
|
|
ctx = get_buffer_context(buf)
|
|
|
|
# do nothing special if there is no context and no active
|
|
# placeholder
|
|
if (not ctx) and (not self.active_placeholder):
|
|
return
|
|
|
|
if not ctx:
|
|
ctx = self.active_placeholder
|
|
|
|
if not ctx in self.ordered_placeholders:
|
|
return
|
|
|
|
# move any marks that were incorrectly moved by this insertion
|
|
# back to where they belong
|
|
begin = ctx.begin_iter()
|
|
end = ctx.end_iter()
|
|
idx = self.ordered_placeholders.index(ctx)
|
|
|
|
for placeholder in self.ordered_placeholders:
|
|
if placeholder == ctx:
|
|
continue
|
|
|
|
ob = placeholder.begin_iter()
|
|
oe = placeholder.end_iter()
|
|
|
|
if ob.compare(begin) == 0 and ((not oe) or oe.compare(end) == 0):
|
|
oidx = self.ordered_placeholders.index(placeholder)
|
|
|
|
if oidx > idx and ob:
|
|
buf.move_mark(placeholder.begin, end)
|
|
elif oidx < idx and oe:
|
|
buf.move_mark(placeholder.end, begin)
|
|
elif ob.compare(begin) >= 0 and ob.compare(end) < 0 and (oe and oe.compare(end) >= 0):
|
|
buf.move_mark(placeholder.begin, end)
|
|
elif (oe and oe.compare(begin) > 0) and ob.compare(begin) <= 0:
|
|
buf.move_mark(placeholder.end, begin)
|
|
|
|
def on_notify_language(self, buf, spec):
|
|
self.update_language()
|
|
|
|
def on_notify_editable(self, view, spec):
|
|
self._set_view(view)
|
|
|
|
def on_view_key_press(self, view, event):
|
|
library = Library()
|
|
|
|
if not (event.state & gdk.CONTROL_MASK) and \
|
|
not (event.state & gdk.MOD1_MASK) and \
|
|
event.keyval in self.TAB_KEY_VAL:
|
|
if not event.state & gdk.SHIFT_MASK:
|
|
return self.run_snippet()
|
|
else:
|
|
return self.skip_to_previous_placeholder()
|
|
elif not library.loaded and \
|
|
library.valid_accelerator(event.keyval, event.state):
|
|
library.ensure_files()
|
|
library.ensure(self.language_id)
|
|
self.accelerator_activate(event.keyval, \
|
|
event.state & gtk.accelerator_get_default_mod_mask())
|
|
|
|
return False
|
|
|
|
def path_split(self, path, components=[]):
|
|
head, tail = os.path.split(path)
|
|
|
|
if not tail and head:
|
|
return [head] + components
|
|
elif tail:
|
|
return self.path_split(head, [tail] + components)
|
|
else:
|
|
return components
|
|
|
|
def relative_path(self, first, second, mime):
|
|
prot1 = re.match('(^[a-z]+:\/\/|\/)(.*)', first)
|
|
prot2 = re.match('(^[a-z]+:\/\/|\/)(.*)', second)
|
|
|
|
if not prot1 or not prot2:
|
|
return second
|
|
|
|
# Different protocols
|
|
if prot1.group(1) != prot2.group(1):
|
|
return second
|
|
|
|
# Split on backslash
|
|
path1 = self.path_split(prot1.group(2))
|
|
path2 = self.path_split(prot2.group(2))
|
|
|
|
# Remove as long as common
|
|
while path1 and path2 and path1[0] == path2[0]:
|
|
path1.pop(0)
|
|
path2.pop(0)
|
|
|
|
# If we need to ../ more than 3 times, then just return
|
|
# the absolute path
|
|
if len(path1) - 1 > 3:
|
|
return second
|
|
|
|
if mime.startswith('x-directory'):
|
|
# directory, special case
|
|
if not path2:
|
|
result = './'
|
|
else:
|
|
result = '../' * (len(path1) - 1)
|
|
else:
|
|
# Insert ../
|
|
result = '../' * (len(path1) - 1)
|
|
|
|
if not path2:
|
|
result = os.path.basename(second)
|
|
|
|
if path2:
|
|
result += os.path.join(*path2)
|
|
|
|
return result
|
|
|
|
def apply_uri_snippet(self, snippet, mime, uri):
|
|
# Remove file scheme
|
|
gfile = gio.File(uri)
|
|
pathname = ''
|
|
dirname = ''
|
|
ruri = ''
|
|
|
|
if pluma.utils.uri_has_file_scheme(uri):
|
|
pathname = gfile.get_path()
|
|
dirname = gfile.get_parent().get_path()
|
|
|
|
name = os.path.basename(uri)
|
|
scheme = gfile.get_uri_scheme()
|
|
|
|
os.environ['PLUMA_DROP_DOCUMENT_URI'] = uri
|
|
os.environ['PLUMA_DROP_DOCUMENT_NAME'] = name
|
|
os.environ['PLUMA_DROP_DOCUMENT_SCHEME'] = scheme
|
|
os.environ['PLUMA_DROP_DOCUMENT_PATH'] = pathname
|
|
os.environ['PLUMA_DROP_DOCUMENT_DIR'] = dirname
|
|
os.environ['PLUMA_DROP_DOCUMENT_TYPE'] = mime
|
|
|
|
buf = self.view.get_buffer()
|
|
location = buf.get_location()
|
|
if location:
|
|
ruri = location.get_uri()
|
|
|
|
relpath = self.relative_path(ruri, uri, mime)
|
|
|
|
os.environ['PLUMA_DROP_DOCUMENT_RELATIVE_PATH'] = relpath
|
|
|
|
mark = buf.get_mark('gtk_drag_target')
|
|
|
|
if not mark:
|
|
mark = buf.get_insert()
|
|
|
|
piter = buf.get_iter_at_mark(mark)
|
|
self.apply_snippet(snippet, piter, piter)
|
|
|
|
def in_bounds(self, x, y):
|
|
rect = self.view.get_visible_rect()
|
|
rect.x, rect.y = self.view.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET, rect.x, rect.y)
|
|
|
|
return not (x < rect.x or x > rect.x + rect.width or y < rect.y or y > rect.y + rect.height)
|
|
|
|
def on_drag_data_received(self, view, context, x, y, data, info, timestamp):
|
|
if not (gtk.targets_include_uri(context.targets) and data.data and self.in_bounds(x, y)):
|
|
return
|
|
|
|
uris = drop_get_uris(data)
|
|
uris.reverse()
|
|
stop = False
|
|
|
|
for uri in uris:
|
|
try:
|
|
mime = gio.content_type_guess(uri)
|
|
except:
|
|
mime = None
|
|
|
|
if not mime:
|
|
continue
|
|
|
|
snippets = Library().from_drop_target(mime, self.language_id)
|
|
|
|
if snippets:
|
|
stop = True
|
|
self.apply_uri_snippet(snippets[0], mime, uri)
|
|
|
|
if stop:
|
|
context.finish(True, False, timestamp)
|
|
view.stop_emission('drag-data-received')
|
|
view.get_toplevel().present()
|
|
view.grab_focus()
|
|
|
|
def find_uri_target(self, context):
|
|
lst = gtk.target_list_add_uri_targets((), 0)
|
|
|
|
return self.view.drag_dest_find_target(context, lst)
|
|
|
|
def on_completion_hide(self, completion):
|
|
self.provider.set_proposals(None)
|
|
|
|
def on_proposal_activated(self, proposal, piter):
|
|
buf = self.view.get_buffer()
|
|
bounds = buf.get_selection_bounds()
|
|
|
|
if bounds:
|
|
self.apply_snippet(proposal.snippet(), None, None)
|
|
else:
|
|
(word, start, end) = self.get_tab_tag(buf, piter)
|
|
self.apply_snippet(proposal.snippet(), start, end)
|
|
|
|
return True
|
|
|
|
def on_default_activated(self, proposal, piter):
|
|
buf = self.view.get_buffer()
|
|
bounds = buf.get_selection_bounds()
|
|
|
|
if bounds:
|
|
buf.begin_user_action()
|
|
buf.delete(bounds[0], bounds[1])
|
|
buf.insert(bounds[0], proposal.props.label)
|
|
buf.end_user_action()
|
|
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def iter_coords(self, piter):
|
|
rect = self.view.get_iter_location(piter)
|
|
rect.x, rect.y = self.view.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, rect.x, rect.y)
|
|
|
|
return rect
|
|
|
|
def placeholder_in_area(self, placeholder, area):
|
|
start = placeholder.begin_iter()
|
|
end = placeholder.end_iter()
|
|
|
|
if not start or not end:
|
|
return False
|
|
|
|
# Test if start is before bottom, and end is after top
|
|
start_rect = self.iter_coords(start)
|
|
end_rect = self.iter_coords(end)
|
|
|
|
return start_rect.y <= area.y + area.height and \
|
|
end_rect.y + end_rect.height >= area.y
|
|
|
|
def draw_placeholder_rect(self, ctx, placeholder, col):
|
|
start = placeholder.begin_iter()
|
|
start_rect = self.iter_coords(start)
|
|
start_line = start.get_line()
|
|
|
|
end = placeholder.end_iter()
|
|
end_rect = self.iter_coords(end)
|
|
end_line = end.get_line()
|
|
|
|
line = start.copy()
|
|
line.set_line_offset(0)
|
|
geom = self.view.get_window(gtk.TEXT_WINDOW_TEXT).get_geometry()
|
|
|
|
ctx.translate(0.5, 0.5)
|
|
|
|
while line.get_line() <= end_line:
|
|
ypos, height = self.view.get_line_yrange(line)
|
|
x_, ypos = self.view.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, 0, ypos)
|
|
|
|
if line.get_line() == start_line and line.get_line() == end_line:
|
|
# Simply draw a box, both are on the same line
|
|
ctx.rectangle(start_rect.x, start_rect.y, end_rect.x - start_rect.x, start_rect.height - 1)
|
|
ctx.stroke()
|
|
elif line.get_line() == start_line or line.get_line() == end_line:
|
|
if line.get_line() == start_line:
|
|
rect = start_rect
|
|
else:
|
|
rect = end_rect
|
|
|
|
ctx.move_to(0, rect.y + rect.height - 1)
|
|
ctx.rel_line_to(rect.x, 0)
|
|
ctx.rel_line_to(0, -rect.height + 1)
|
|
ctx.rel_line_to(geom[2], 0)
|
|
ctx.stroke()
|
|
|
|
if not line.forward_line():
|
|
break
|
|
|
|
def draw_placeholder_bar(self, ctx, placeholder, col):
|
|
start = placeholder.begin_iter()
|
|
start_rect = self.iter_coords(start)
|
|
|
|
ctx.translate(0.5, 0.5)
|
|
extend_width = 2.5
|
|
|
|
ctx.move_to(start_rect.x - extend_width, start_rect.y)
|
|
ctx.rel_line_to(extend_width * 2, 0)
|
|
|
|
ctx.move_to(start_rect.x, start_rect.y)
|
|
ctx.rel_line_to(0, start_rect.height - 1)
|
|
|
|
ctx.rel_move_to(-extend_width, 0)
|
|
ctx.rel_line_to(extend_width * 2, 0)
|
|
ctx.stroke()
|
|
|
|
def from_color(self, col):
|
|
return [col.red / 0x10000, col.green / 0x10000, col.blue / 0x10000]
|
|
|
|
def draw_placeholder(self, ctx, placeholder):
|
|
if isinstance(placeholder, PlaceholderEnd):
|
|
return
|
|
|
|
buf = self.view.get_buffer()
|
|
|
|
col = self.from_color(self.view.get_style().text[gtk.STATE_INSENSITIVE])
|
|
ctx.set_source_rgba(col[0], col[1], col[2], 0.5)
|
|
|
|
if placeholder.tabstop > 0:
|
|
ctx.set_dash([], 0)
|
|
else:
|
|
ctx.set_dash([2], 0)
|
|
|
|
start = placeholder.begin_iter()
|
|
end = placeholder.end_iter()
|
|
|
|
if start.equal(end):
|
|
self.draw_placeholder_bar(ctx, placeholder, col)
|
|
else:
|
|
self.draw_placeholder_rect(ctx, placeholder, col)
|
|
|
|
def on_expose_event(self, view, event):
|
|
if event.window != view.get_window(gtk.TEXT_WINDOW_TEXT):
|
|
return False
|
|
|
|
# Draw something
|
|
ctx = event.window.cairo_create()
|
|
ctx.rectangle(event.area)
|
|
ctx.clip()
|
|
|
|
ctx.set_line_width(1.0)
|
|
|
|
for placeholder in self.ordered_placeholders:
|
|
if not self.placeholder_in_area(placeholder, event.area):
|
|
continue
|
|
|
|
ctx.save()
|
|
self.draw_placeholder(ctx, placeholder)
|
|
ctx.restore()
|
|
|
|
return False
|
|
|
|
# ex:ts=8:et:
|