# -*- coding: utf-8 -*-
# DocumentWrapper.py
# This file is part of the Gedit LaTeX Plugin
#
# Copyright (C) 2006 Michael Zeising
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public Licence as published by the Free Software
# Foundation; either version 2 of the Licence, 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 Licence for more
# details.
#
# You should have received a copy of the GNU General Public Licence along with
# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
# Street, Fifth Floor, Boston, MA 02110-1301, USA
import gtk, gobject, re
from completion import CompletionProvider
import const
LEVELS = ["part", "chapter", "section", "subsection", "subsubsection", "paragraph", "subparagraph"]
# keys recognized, when the completion popup is visible
validCompletionKeys = ["Up", "Down", "Page_Up", "Page_Down", "Escape", "Return"]
class DocumentWrapper(gobject.GObject):
"""
Encapsulates an open document
"""
__gsignals__ = {
"environment-changed" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"structure-updated" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
}
environment = const.UNKNOWN
_uri = None
_changed = True
_masterFilename = None
# these re's may compiled on demand
reIndent, reStructure, reLabelOrBibitem, reNewcommand, reEnvironment, reDocument = None, None, None, None, None, None
messageDialog = None
def __init__(self, textView, instance):
gobject.GObject.__init__(self)
self.reComment = re.compile(r"%.*?$", re.M)
self.rePackage = re.compile(r"\\usepackage(\[(.*?)\])?\{(.*?)\}", re.M)
self._instance = instance
self._textView = textView
self._textBuffer = textView.get_buffer()
self.modelStructure = gtk.TreeStore(str, str) # name, complete string
self.modelLabelsAndBibs = gtk.ListStore(str, int, str) # name, type (0:label, 1:bibitem), complete string
self.modelNewcommands = gtk.ListStore(str, str, str, str) # name, definition, markup (name), complete string
self.modelPackages = gtk.ListStore(str, str) # package, options
self._completionProvider = CompletionProvider(instance._builtinCompletionTargets, self.modelNewcommands, self.modelLabelsAndBibs, self.modelPackages)
self._textBuffer.connect("changed", self.__onBufferChanged)
#self._textBuffer.connect("end-user-action", self.__onEndUserAction)
#if instance._prefs.setdefault("/auto-closing-enabled", True):
# self.insertHandler = self._textBuffer.connect("insert-text", self._onInsert)
textView.connect("key-release-event", lambda *a: self._recognizeEnvironment())
textView.connect("button-release-event", lambda *a: self._recognizeEnvironment())
if instance._prefs.setdefault("/auto-completion-enabled", True):
textView.connect_after("key-release-event", self._onKeyReleaseEvent)
textView.connect("key-press-event", self._onKeyPressEvent)
textView.connect("focus-out-event", self._onFocusOutEvent)
def _isFragment(self):
""" Returns True if this document seems to be only a fragment """
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False)
return (string.find(r"\begin{document}") == -1)
def _getMasterFilename(self):
""" Returns the filename of this document's master """
# return cached filename if available
if self._masterFilename:
return self._masterFilename
# reset
self._masterFilename = None
ownFilename = self.getFilename()
if ownFilename:
import os, glob
docName = os.path.splitext(os.path.basename(ownFilename))[0]
for filename in glob.glob(os.path.dirname(ownFilename) + "/*.tex"):
contents = open(filename).read()
if contents.find(r"\include{" + docName) != -1: # this one includes the file
if contents.find(r"\begin{document}") != -1: # and it's a full document >> bingo!
self._masterFilename = filename
break
return self._masterFilename
def _onFocusOutEvent(self, textView, event):
self._instance._completionPopup.hide()
def _onKeyPressEvent(self, textView, event):
if self._instance._completionPopup.get_property("visible"):
keyVal = gtk.gdk.keyval_name(event.keyval)
if keyVal in validCompletionKeys:
path = self._instance._completionPopup.treeView.get_cursor()[0]
if path:
index = path[0]
maxIndex = self._instance._completionPopup.model.iter_n_children(None)
keyVal = gtk.gdk.keyval_name(event.keyval)
if keyVal == "Return":
# accept the selected target
listStore, listIter = self._instance._completionPopup.treeView.get_selection().get_selected()
target = listStore.get_value(listIter, 0)
self._textBuffer.insert_at_cursor(target[len(self._completionProvider.lastPrefix):])
self.__autoClose()
self._instance._completionPopup.hide()
return True
elif keyVal == "Escape":
# close the popup
self._instance._completionPopup.hide()
return True
if keyVal == "Up":
index -= 1
elif keyVal == "Down":
index += 1
elif keyVal == "Page_Up":
index -= 5
elif keyVal == "Page_Down":
index += 5
if index < 0:
index = 0
elif index >= maxIndex:
index = maxIndex - 1
self._instance._completionPopup.treeView.set_cursor((index))
return True
else:
return False
def __autoClose(self):
""" perform auto-closing """
insIter = self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert())
limitIter = insIter.copy()
limitIter.backward_chars(30)
match = insIter.backward_search("\\", False, limitIter)
if not match:
return False
if not self.reEnvironment:
self.reEnvironment = re.compile(r"begin\{([a-zA-Z]+)\}$")
string = self._textBuffer.get_text(insIter, match[1], False)
match = self.reEnvironment.match(string)
if match:
if not self.isLatex():
return False
indent = self._getIndent(insIter.get_line())
closeString = "\n%s\t\n%s\\end{%s}" % (indent, indent, match.group(1))
mark = self._textBuffer.create_mark(None, self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert()), True)
self._textBuffer.insert_at_cursor(closeString)
insIter = self._textBuffer.get_iter_at_mark(mark)
insIter.forward_line()
insIter.forward_to_line_end()
self._textBuffer.place_cursor(insIter)
self._textBuffer.delete_mark(mark)
def _onKeyReleaseEvent(self, textView, event):
""" Try to complete a written command using self._completionProvider and show
the self._instance._completionPopup """
keyVal = gtk.gdk.keyval_name(event.keyval)
if keyVal == "Escape":
return True
if self._instance._completionPopup.get_property("visible"):
if keyVal in validCompletionKeys:
# Signal bezieht sich auf Popup, nicht weitergeben, also blockieren
return True
insIter = self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert())
if keyVal == "braceright":
self._instance._completionPopup.hide()
self.__autoClose()
return False
if not insIter.ends_word():
self._instance._completionPopup.hide()
return False
# look for command
limitIter = insIter.copy()
limitIter.backward_chars(30)
match = insIter.backward_search("\\", False, limitIter)
if not match:
self._instance._completionPopup.hide()
return False
# FIXME: this is very bad, signal handlers should not be connected until
# there is LaTeX in the document to save resources
if not self.isLatex():
self._instance._completionPopup.hide()
return False
string = self._textBuffer.get_text(insIter, match[1], False)
# find targets
completionList = self._completionProvider.complete(string)
numOfMatches = len(completionList)
if not numOfMatches or string in [match[0] for match in completionList]:
self._instance._completionPopup.hide()
return False
# store matches in the model
self._instance._completionPopup.model.clear()
for match in completionList:
self._instance._completionPopup.model.append([match[0], match[1]])
self._instance._completionPopup.treeView.set_cursor((0))
# calculate iter coords
rect = self._textView.get_iter_location(self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert()))
x, y = self._textView.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET, rect.x, rect.y)
# add widget coords
rootX, rootY = self._textView.get_window(gtk.TEXT_WINDOW_WIDGET).get_geometry()[:2]
x += rootX
y += rootY
# add window coords
rootX, rootY = self._instance._window.get_position()
x += rootX
y += rootY
# move and show the popup
self._instance._completionPopup.move(x + 16, y + 18)
self._instance._completionPopup.show_all()
return True
def update(self):
""" Called when the document is activated on tab change """
uri = self._textBuffer.get_uri()
if uri != self._uri or self._changed:
self._uri = uri
self._changed = False
self.update()
def findString(self, string):
""" finds a string in the buffer and selects it """
iter = self._textBuffer.get_start_iter()
result = iter.forward_search(string, False)
if result:
self._textBuffer.select_range(result[0], result[1])
self._textView.scroll_to_iter(result[0], .25)
def getString(self):
""" returns the text in the buffer """
return self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter())
def getFilename(self):
import gnomevfs
""" returns the document's filename extracted from its URI """
uri = self._textBuffer.get_uri()
if uri and gnomevfs.get_uri_scheme(uri) == "file":
return gnomevfs.get_local_path_from_uri(uri)
return None
def comment_selection(self, uncomment=False):
""" (un-)comments the currently selected line(s) """
bounds = self._textBuffer.get_selection_bounds()
if len(bounds):
for line_number in xrange(bounds[0].get_line(), bounds[1].get_line() + 1):
start_iter = self._textBuffer.get_iter_at_line(line_number)
end_iter = self._textBuffer.get_iter_at_line_offset(line_number, 1)
if uncomment and self._textBuffer.get_text(start_iter, end_iter) == "%":
# uncomment
self._textBuffer.delete(start_iter, end_iter)
else:
# comment
self._textBuffer.insert(start_iter, "%")
else:
insert = self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert())
start_iter = self._textBuffer.get_iter_at_line(insert.get_line())
end_iter = self._textBuffer.get_iter_at_line_offset(insert.get_line(), 1)
if uncomment and self._textBuffer.get_text(start_iter, end_iter) == "%":
# uncomment
self._textBuffer.delete(start_iter, end_iter)
else:
# comment
self._textBuffer.insert(start_iter, "%")
def updatePackages(self, completeUpdate=False, string=None):
""" updates the list of included packages """
# \usepackage commands may only be used in the preamble
# so if we are a fragment, only the master's packages have to be parsed
if self._isFragment():
masterFilename = self._getMasterFilename()
if not masterFilename:
return
string = open(masterFilename).read().decode("utf-8")
else:
if not completeUpdate:
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False).decode("utf-8")
self.modelPackages.clear()
for match in self.rePackage.finditer(string):
self.modelPackages.append([match.group(3), match.group(2)])
# update the CompletionStore
if not completeUpdate:
self._completionProvider.update()
def updateLabelsAndBibitems(self, completeUpdate=False, string=None):
""" updates the list of \labelS and \bibitemS """
if not self.reLabelOrBibitem:
# TODO: match the complete \bibitem to be able to show it as a hint
self.reLabelOrBibitem = re.compile(r"\\(label|bibitem)(\[.*?\])?\{(.*?)\}", re.M)
if not completeUpdate:
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False).decode("utf-8")
# \labelS may also be defined in the master document
# so this must be parsed, too
if self._isFragment():
masterFilename = self._getMasterFilename()
if masterFilename:
stringMaster = open(masterFilename).read().decode("utf-8")
string += stringMaster
self.modelLabelsAndBibs.clear()
for match in self.reLabelOrBibitem.finditer(string):
if match.group(1) == "label":
self.modelLabelsAndBibs.append([match.group(3), const.LABEL, match.group(0)])
else:
self.modelLabelsAndBibs.append([match.group(3), const.BIBITEM, match.group(0)])
# update the CompletionStore
if not completeUpdate:
self._completionProvider.update()
def updateNewcommands(self, completeUpdate=False, string=None):
""" updates the list of newcommands
completeUpdate: this call is part of a complete parsing
string: content of the TextBuffer if completeUpdate == True """
if not self.reNewcommand:
# FIXME: 2nd parameter may only be [1-9]
self.reNewcommand = re.compile(r"\\newcommand\{(.*?)\}(\[[1-9]?\])?\{(.*)\}", re.M)
if not completeUpdate:
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False).decode("utf-8")
# \newcommandS may also be defined in the master document
# so this must be parsed, too
if self._isFragment():
masterFilename = self._getMasterFilename()
if masterFilename:
stringMaster = open(masterFilename).read().decode("utf-8")
string += stringMaster
self.modelNewcommands.clear()
for match in self.reNewcommand.finditer(string):
self.modelNewcommands.append([match.group(1), match.group(3), "%s" % match.group(1), match.group(0)])
# update the CompletionStore
if not completeUpdate:
self._completionProvider.update()
def updateStructure(self, completeUpdate=False, string=None):
""" updates the structure tree
completeUpdate: this call is part of a complete parsing
string: content of the TextBuffer if completeUpdate == True """
# TODO: if we are only a fragment, then embed our structure into the one
# of the master document
if not self.reStructure:
self.reStructure = re.compile(r"\\(part|chapter|section|subsection|subsubsection|paragraph|subparagraph|include)\*?(\[.*?\])?(\{.*\})", re.M)
if not completeUpdate:
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False).decode("utf-8")
self.modelStructure.clear()
lastAbsLevel = 0 # last absolute level of element
treeOffset = 0 # current offset in the tree
parentNodes = [None]
for match in self.reStructure.finditer(string):
# match brackets
argument = match.group(3)
depth = 0
i = 1
while i < len(argument):
if argument[i] == "{":
depth += 1
elif argument[i] == "}":
depth -= 1
i += 1
if depth == -1:
break
argument = argument[1:i-1].replace("&", "&")
if match.group(1) == "include":
node = self.modelStructure.append(parentNodes[treeOffset], ["%s" % argument, match.group(0)])
else:
absLevel = LEVELS.index(match.group(1)) # absolute level of element
delta = absLevel - lastAbsLevel # delta of levels
lastAbsLevel = absLevel
if delta >= 1:
treeOffset += 1
else:
treeOffset += delta
if treeOffset < 1:
treeOffset = 1
node = self.modelStructure.append(parentNodes[treeOffset - 1], [argument, match.group(0)])
try:
parentNodes[treeOffset] = node
except IndexError:
parentNodes.append(node)
self.emit("structure-updated")
def update(self):
"""
Parses the document and updates the structure, labels and newcommands
models
"""
# no text?
if not self._textBuffer.get_end_iter().get_offset():
return
if not self.isLatex():
return
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter(), False).decode("utf-8")
# kill comments
string = self.reComment.sub("", string)
self.updateStructure(True, string)
self.updateLabelsAndBibitems(True, string)
self.updateNewcommands(True, string)
self.updatePackages(True, string)
self._completionProvider.update()
def isLatex(self):
"""
Returns True if the document appears to contain LaTeX
"""
language = self._textBuffer.get_language()
if language and language.get_id() == "LaTeX":
return True
uri = self._textBuffer.get_uri()
if uri and uri.endswith(".tex"):
return True
return False
def getLine(self):
""" returns the current line number """
return self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert()).get_line()
def _getIndent(self, lineNumber=-1):
"""
Returns the indentation of the current line or a given line number
"""
if not self.reIndent:
self.reIndent = re.compile("[ \t]+")
if lineNumber > -1:
startIter = self._textBuffer.get_iter_at_line(lineNumber)
else:
startIter = self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert())
startIter.set_line_offset(0)
endIter = startIter.copy()
endIter.forward_to_line_end()
string = self._textBuffer.get_text(startIter, endIter)
match = self.reIndent.match(string)
if match:
return match.group()
else:
return ""
def scrollToLine(self, line):
"""
Scrolls the document to a given line
"""
self._textView.scroll_to_iter(self._textBuffer.get_iter_at_line(line), .25)
def _get_line_number_from_offset(self, offset):
"""
Returns the line number that corresponds to an offset
"""
return self._textBuffer.get_iter_at_offset(offset).get_line()
def insert(self, arg, line_number=-1):
"""
Inserts text into the document
@param arg: the text to be inserted.
If this is a list and there's _no_ selection, then its items are
inserted successively and the cursor is placed after the first
one.
If it's a list and there _is_ a selection, then its first item
is inserted at the selection's start and the following ones are
inserted at its end successively.
@param line_number: if > -1, the text is inserted after this line
@param restoreIndent: if True, the indentation of the current line
is restored and continued in the following lines
"""
if line_number > -1:
# get indent at given line and prepend it
indent = self._getIndent(line_number)
arg = "%s%s" % (indent, "".join(arg))
# TODO: follow indent on multiline inserts
self._textBuffer.insert(self._textBuffer.get_iter_at_line(line_number), arg)
else:
indent = self._getIndent()
if type(arg) is str: # one single string
self._textBuffer.insert_at_cursor(arg.replace("\n", "\n" + indent))
elif type(arg) is tuple: # multiple strings
bounds = self._textBuffer.get_selection_bounds()
if len(bounds):
# store the selection marks
l_mark = self._textBuffer.create_mark(None, bounds[0], False)
r_mark = self._textBuffer.create_mark(None, bounds[1], True)
# insert first piece at the beginning...
self._textBuffer.insert(bounds[0], arg[0])
# ...and the other ones at the end
self._textBuffer.insert(self._textBuffer.get_iter_at_mark(r_mark), "".join(arg[1:]))
# restore selection
self._textBuffer.select_range(self._textBuffer.get_iter_at_mark(l_mark), self._textBuffer.get_iter_at_mark(r_mark))
# delete marks
self._textBuffer.delete_mark(l_mark)
self._textBuffer.delete_mark(r_mark)
else:
# insert first item
head = arg[0].replace("\n", "\n" + indent)
self._textBuffer.insert_at_cursor(head)
# copy the insert mark
mark = self._textBuffer.create_mark(None, self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert()), True)
# insert the remaining pieces
tail = "".join(arg[1:]).replace("\n", "\n" + indent)
self._textBuffer.insert_at_cursor(tail)
# place cursor after the first one
self._textBuffer.place_cursor(self._textBuffer.get_iter_at_mark(mark))
# delete mark
self._textBuffer.delete_mark(mark)
self._recognizeEnvironment()
self._textView.grab_focus()
def enable_highlight(self):
"""
Sets the document's source language to LaTeX and enables highlighting
"""
import gtksourceview
manager = gtksourceview.SourceLanguagesManager()
language = manager.get_language_from_mime_type("text/x-tex")
self._textBuffer.set_language(language)
self._textBuffer.set_highlight(True)
def requirePackages(self, packages):
if not len(packages):
return
if not self.reDocument:
self.reDocument = re.compile(r"(\\documentclass|\\documentstyle)(\[[A-Za-z0-9,]*\])*\{[A-Za-z0-9,]*\}")
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_end_iter())
# filter packages not yet included and bundle them to one string
filteredPackages = [p for p in packages if not re.compile(r"\\usepackage\{%s\}" % p).search(string)]
bundle = "".join(["\\usepackage{%s}\n" % p for p in filteredPackages])
# find a line to insert the string
result = self.rePackage.search(string)
if result: # there are already included packages, place it at the start of them
lineNumber = self._get_line_number_from_offset(result.span()[0])
else: # there are no packages yet
result = self.reDocument.search(string)
if result: # place it after document start
lineNumber = self._get_line_number_from_offset(result.span()[1]) + 1
else: # no document at all, place it at the start
lineNumber = 0
# insert
self.insert(bundle, lineNumber)
# update packages
self.updatePackages()
# popup message
if not self.messageDialog:
self.messageDialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, None)
if len(filteredPackages) > 1:
self.messageDialog.set_markup("The inserted command required the additional LaTeX packages %s. They have been added to your document." % ", ".join(filteredPackages))
else:
self.messageDialog.set_markup("The inserted command required the additional LaTeX package %s. It has been added to your document." % filteredPackages[0])
self.messageDialog.run()
self.messageDialog.hide()
def insert_newcommand(self, name, definition, refCount=0):
"""
Inserts a \newcommand into the document
"""
text = self.getString()
refStr = ""
if refCount:
refStr = "[%s]" % refCount
if re.compile(r"\\newcommand\{%s\}" % re.escape(name)).search(text):
# the name of this newcommand is already in use
message = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, None)
message.set_markup("The document already contains a new command named %s." % name)
message.run()
message.destroy()
self.updateNewcommands()
return
result = re.compile(r"\\usepackage(\[[A-Za-z0-9,]*\])*\{[A-Za-z0-9,]*\}").search(text)
if result:
# place this \newcommand after the last one found in the document
line_number = self._get_line_number_from_offset(result.span()[0])
self.insert("\\newcommand{%s}%s{%s}\n" % (name, refStr, definition), line_number)
self.updateNewcommands()
return
result = re.compile(r"(\\documentclass|\\documentstyle)(\[[A-Za-z0-9,]*\])*\{[A-Za-z0-9,]*\}").search(text)
if result:
# place it after document start
line_number = self._get_line_number_from_offset(result.span()[1])
self.insert("\n\\newcommand{%s}%s{%s}" % (name, refStr, definition), line_number)
self.updateNewcommands()
return
# insert it at the start of the file
self.insert("\\newcommand{%s}%s{%s}\n" % (name, refStr, definition), 0)
self.updateNewcommands()
def remove_newcommand(self, name):
pass
def _recognizeEnvironment(self):
""" Recognize the environment the cursor is in """
# FIXME: only parse when the buffer has changed or the cursor has moved
# (don't parse on Ctrl or Alt)
if self.isLatex():
string = self._textBuffer.get_text(self._textBuffer.get_start_iter(), self._textBuffer.get_iter_at_mark(self._textBuffer.get_insert()))
string = self.reComment.sub("", string)
counter = 0
for item in [r"$", r"{math", r"{displaymath", r"{eqnarray", r"\[", r"\]"]:
counter += string.count(item)
if counter % 2:
environment = const.MATH
else:
environment = const.GENERAL
if environment != self.environment:
self.environment = environment
self.emit("environment-changed")
else:
environment = const.UNKNOWN
if environment != self.environment:
self.environment = environment
self.emit("environment-changed")
def __onBufferChanged(self, textBuffer):
self._changed = True
# register GObject
gobject.type_register(DocumentWrapper)