mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Diff tool: Add syntax highlighting for many other filetypes
Now I can use the diff tool as my primary diff tool in all contexts. Syntax highlighting for non-core filetypes is courtesy of pygments.
This commit is contained in:
parent
f306d68f6e
commit
3ff5b2bb99
151
src/calibre/gui2/tweak_book/diff/highlight.py
Normal file
151
src/calibre/gui2/tweak_book/diff/highlight.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt4.Qt import QTextDocument, QTextCursor, QTextCharFormat, QPlainTextDocumentLayout
|
||||||
|
|
||||||
|
from calibre.gui2.tweak_book import tprefs
|
||||||
|
from calibre.gui2.tweak_book.editor.text import get_highlighter as calibre_highlighter, SyntaxHighlighter
|
||||||
|
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, highlight_to_char_format
|
||||||
|
|
||||||
|
def get_theme():
|
||||||
|
theme = THEMES.get(tprefs['editor_theme'], None)
|
||||||
|
if theme is None:
|
||||||
|
theme = THEMES[default_theme()]
|
||||||
|
return theme
|
||||||
|
|
||||||
|
NULL_FMT = QTextCharFormat()
|
||||||
|
|
||||||
|
class QtHighlighter(QTextDocument):
|
||||||
|
|
||||||
|
def __init__(self, parent, text, hlclass):
|
||||||
|
QTextDocument.__init__(self, parent)
|
||||||
|
self.l = QPlainTextDocumentLayout(self)
|
||||||
|
self.setDocumentLayout(self.l)
|
||||||
|
self.highlighter = hlclass(self)
|
||||||
|
self.highlighter.apply_theme(get_theme())
|
||||||
|
self.highlighter.setDocument(self)
|
||||||
|
self.setPlainText(text)
|
||||||
|
|
||||||
|
def copy_lines(self, lo, hi, cursor):
|
||||||
|
''' Copy specified lines from the syntax highlighted buffer into the
|
||||||
|
destination cursor, preserving all formatting created by the syntax
|
||||||
|
highlighter. '''
|
||||||
|
num = hi - lo
|
||||||
|
if num > 0:
|
||||||
|
block = self.findBlockByNumber(lo)
|
||||||
|
while num > 0:
|
||||||
|
num -= 1
|
||||||
|
cursor.insertText(block.text())
|
||||||
|
dest_block = cursor.block()
|
||||||
|
c = QTextCursor(dest_block)
|
||||||
|
for af in block.layout().additionalFormats():
|
||||||
|
start = dest_block.position() + af.start
|
||||||
|
c.setPosition(start), c.setPosition(start + af.length, c.KeepAnchor)
|
||||||
|
c.setCharFormat(af.format)
|
||||||
|
cursor.insertBlock()
|
||||||
|
cursor.setCharFormat(NULL_FMT)
|
||||||
|
block = block.next()
|
||||||
|
|
||||||
|
class NullHighlighter(object):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
self.lines = text.splitlines()
|
||||||
|
|
||||||
|
def copy_lines(self, lo, hi, cursor):
|
||||||
|
for i in xrange(lo, hi):
|
||||||
|
cursor.insertText(self.lines[i])
|
||||||
|
cursor.insertBlock()
|
||||||
|
|
||||||
|
def pygments_lexer(filename):
|
||||||
|
try:
|
||||||
|
from pygments.lexers import get_lexer_for_filename
|
||||||
|
from pygments.util import ClassNotFound
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return get_lexer_for_filename(filename)
|
||||||
|
except ClassNotFound:
|
||||||
|
if filename.lower().endswith('.recipe'):
|
||||||
|
return get_lexer_for_filename('a.py')
|
||||||
|
return None
|
||||||
|
|
||||||
|
_pyg_map = None
|
||||||
|
def pygments_map():
|
||||||
|
global _pyg_map
|
||||||
|
if _pyg_map is None:
|
||||||
|
from pygments.token import Token
|
||||||
|
_pyg_map = {
|
||||||
|
Token: None,
|
||||||
|
Token.Comment: 'Comment',
|
||||||
|
Token.Comment.Preproc: 'PreProc',
|
||||||
|
Token.String: 'String',
|
||||||
|
Token.Number: 'Number',
|
||||||
|
Token.Keyword.Type: 'Type',
|
||||||
|
Token.Keyword: 'Keyword',
|
||||||
|
Token.Name.Builtin: 'Identifier',
|
||||||
|
Token.Operator: 'Statement',
|
||||||
|
Token.Name.Function: 'Function',
|
||||||
|
Token.Literal: 'Constant',
|
||||||
|
Token.Error: 'Error',
|
||||||
|
}
|
||||||
|
return _pyg_map
|
||||||
|
|
||||||
|
def format_for_token(theme, cache, token):
|
||||||
|
try:
|
||||||
|
return cache[token]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
pmap = pygments_map()
|
||||||
|
while token is not None:
|
||||||
|
try:
|
||||||
|
name = pmap[token]
|
||||||
|
except KeyError:
|
||||||
|
token = token.parent
|
||||||
|
continue
|
||||||
|
cache[token] = ans = theme[name]
|
||||||
|
return ans
|
||||||
|
cache[token] = ans = NULL_FMT
|
||||||
|
return ans
|
||||||
|
|
||||||
|
class PygmentsHighlighter(object):
|
||||||
|
|
||||||
|
def __init__(self, text, lexer):
|
||||||
|
theme, cache = get_theme(), {}
|
||||||
|
theme = {k:highlight_to_char_format(v) for k, v in theme.iteritems()}
|
||||||
|
theme[None] = NULL_FMT
|
||||||
|
def fmt(token):
|
||||||
|
return format_for_token(theme, cache, token)
|
||||||
|
|
||||||
|
from pygments import lex
|
||||||
|
lines = self.lines = [[]]
|
||||||
|
current_line = lines[0]
|
||||||
|
for token, val in lex(text, lexer):
|
||||||
|
for v in val.splitlines(True):
|
||||||
|
current_line.append((fmt(token), v))
|
||||||
|
if v[-1] in '\n\r':
|
||||||
|
lines.append([])
|
||||||
|
current_line = lines[-1]
|
||||||
|
continue
|
||||||
|
|
||||||
|
def copy_lines(self, lo, hi, cursor):
|
||||||
|
for i in xrange(lo, hi):
|
||||||
|
for fmt, text in self.lines[i]:
|
||||||
|
cursor.insertText(text, fmt)
|
||||||
|
cursor.setCharFormat(NULL_FMT)
|
||||||
|
|
||||||
|
def get_highlighter(parent, text, syntax):
|
||||||
|
hlclass = calibre_highlighter(syntax)
|
||||||
|
if hlclass is SyntaxHighlighter:
|
||||||
|
filename = os.path.basename(parent.headers[-1][1])
|
||||||
|
lexer = pygments_lexer(filename)
|
||||||
|
if lexer is None:
|
||||||
|
return NullHighlighter(text)
|
||||||
|
return PygmentsHighlighter(text, lexer)
|
||||||
|
return QtHighlighter(parent, text, hlclass)
|
@ -16,7 +16,7 @@ from future_builtins import zip
|
|||||||
|
|
||||||
import regex
|
import regex
|
||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QSplitter, QApplication, QPlainTextDocumentLayout, QTextDocument, QTimer,
|
QSplitter, QApplication, QTimer,
|
||||||
QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette, QPen, QBrush,
|
QTextCursor, QTextCharFormat, Qt, QRect, QPainter, QPalette, QPen, QBrush,
|
||||||
QColor, QTextLayout, QCursor, QFont, QSplitterHandle, QPainterPath,
|
QColor, QTextLayout, QCursor, QFont, QSplitterHandle, QPainterPath,
|
||||||
QHBoxLayout, QWidget, QScrollBar, QEventLoop, pyqtSignal, QImage, QPixmap,
|
QHBoxLayout, QWidget, QScrollBar, QEventLoop, pyqtSignal, QImage, QPixmap,
|
||||||
@ -25,9 +25,10 @@ from PyQt4.Qt import (
|
|||||||
from calibre import human_readable, fit_image
|
from calibre import human_readable, fit_image
|
||||||
from calibre.gui2 import info_dialog
|
from calibre.gui2 import info_dialog
|
||||||
from calibre.gui2.tweak_book import tprefs
|
from calibre.gui2.tweak_book import tprefs
|
||||||
from calibre.gui2.tweak_book.editor.text import PlainTextEdit, get_highlighter, default_font_family, LineNumbers
|
from calibre.gui2.tweak_book.editor.text import PlainTextEdit, default_font_family, LineNumbers
|
||||||
from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color
|
from calibre.gui2.tweak_book.editor.themes import theme_color
|
||||||
from calibre.gui2.tweak_book.diff import get_sequence_matcher
|
from calibre.gui2.tweak_book.diff import get_sequence_matcher
|
||||||
|
from calibre.gui2.tweak_book.diff.highlight import get_theme, get_highlighter
|
||||||
|
|
||||||
Change = namedtuple('Change', 'ltop lbot rtop rbot kind')
|
Change = namedtuple('Change', 'ltop lbot rtop rbot kind')
|
||||||
|
|
||||||
@ -39,12 +40,6 @@ class BusyCursor(object):
|
|||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
QApplication.restoreOverrideCursor()
|
QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
def get_theme():
|
|
||||||
theme = THEMES.get(tprefs['editor_theme'], None)
|
|
||||||
if theme is None:
|
|
||||||
theme = THEMES[default_theme()]
|
|
||||||
return theme
|
|
||||||
|
|
||||||
def beautify_text(raw, syntax):
|
def beautify_text(raw, syntax):
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre.ebooks.oeb.polish.parsing import parse
|
from calibre.ebooks.oeb.polish.parsing import parse
|
||||||
@ -392,38 +387,6 @@ class TextBrowser(PlainTextEdit): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Highlight(QTextDocument): # {{{
|
|
||||||
|
|
||||||
def __init__(self, parent, text, syntax):
|
|
||||||
QTextDocument.__init__(self, parent)
|
|
||||||
self.l = QPlainTextDocumentLayout(self)
|
|
||||||
self.setDocumentLayout(self.l)
|
|
||||||
self.highlighter = get_highlighter(syntax)(self)
|
|
||||||
self.highlighter.apply_theme(get_theme())
|
|
||||||
self.highlighter.setDocument(self)
|
|
||||||
self.setPlainText(text)
|
|
||||||
|
|
||||||
def copy_lines(self, lo, hi, cursor):
|
|
||||||
''' Copy specified lines from the syntax highlighted buffer into the
|
|
||||||
destination cursor, preserving all formatting created by the syntax
|
|
||||||
highlighter. '''
|
|
||||||
num = hi - lo
|
|
||||||
if num > 0:
|
|
||||||
block = self.findBlockByNumber(lo)
|
|
||||||
while num > 0:
|
|
||||||
num -= 1
|
|
||||||
cursor.insertText(block.text())
|
|
||||||
dest_block = cursor.block()
|
|
||||||
c = QTextCursor(dest_block)
|
|
||||||
for af in block.layout().additionalFormats():
|
|
||||||
start = dest_block.position() + af.start
|
|
||||||
c.setPosition(start), c.setPosition(start + af.length, c.KeepAnchor)
|
|
||||||
c.setCharFormat(af.format)
|
|
||||||
cursor.insertBlock()
|
|
||||||
cursor.setCharFormat(QTextCharFormat())
|
|
||||||
block = block.next()
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class DiffSplitHandle(QSplitterHandle): # {{{
|
class DiffSplitHandle(QSplitterHandle): # {{{
|
||||||
|
|
||||||
WIDTH = 30 # px
|
WIDTH = 30 # px
|
||||||
@ -697,7 +660,7 @@ class DiffSplit(QSplitter): # {{{
|
|||||||
|
|
||||||
cruncher = get_sequence_matcher()(None, left_lines, right_lines)
|
cruncher = get_sequence_matcher()(None, left_lines, right_lines)
|
||||||
|
|
||||||
left_highlight, right_highlight = Highlight(self, left_text, syntax), Highlight(self, right_text, syntax)
|
left_highlight, right_highlight = get_highlighter(self.left, left_text, syntax), get_highlighter(self.right, right_text, syntax)
|
||||||
cl, cr = self.left_cursor, self.right_cursor = self.left.textCursor(), self.right.textCursor()
|
cl, cr = self.left_cursor, self.right_cursor = self.left.textCursor(), self.right.textCursor()
|
||||||
cl.beginEditBlock(), cr.beginEditBlock()
|
cl.beginEditBlock(), cr.beginEditBlock()
|
||||||
cl.movePosition(cl.End), cr.movePosition(cr.End)
|
cl.movePosition(cl.End), cr.movePosition(cr.End)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user