Source code for TermTk.TTkGui.textdocument_highlight_pygments

# MIT License
#
# Copyright (c) 2024 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TextDocumentHighlight']

from pygments import highlight
from pygments.util import ClassNotFound
from pygments.styles import get_all_styles
from pygments.lexers import guess_lexer, guess_lexer_for_filename, get_lexer_by_name, special, get_all_lexers
from pygments.formatters import TerminalFormatter, Terminal256Formatter, TerminalTrueColorFormatter
from pygments.formatter import Formatter
from pygments.token import Keyword, Name, Comment, String, Error, \
    Number, Operator, Generic, Token, Whitespace

from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.string import TTkString
from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal
from TermTk.TTkCore.timer import TTkTimer

from TermTk.TTkGui.textdocument import TTkTextDocument

class _TTkFormatter(Formatter):
    class Data():
        __slots__=('lines', 'block', 'error', 'multiline')
        def __init__(self, lines, block):
            self.lines = lines
            self.block = block
            self.error = None
            self.multiline = False

    __slots__ = ('_dl', '_blockNum', '_highlightStyles', '_defaultColor')
    def __init__(self, *args, **kwargs):
        self._defaultColor = TTkColor.RST
        super().__init__(*args, **kwargs)
        self._highlightStyles = {}
        self._blockNum = 1
        for token, style in self.style:
            # Token = Token.Comment.PreprocFile
            # style = {
            #   'color': '6272a4',
            #   'bgcolor': None,
            #   'bold': False, 'italic': False, 'underline': False,
            #   'border': None,
            #   'roman': None, 'sans': None, 'mono': None,
            #   'ansicolor': None, 'bgansicolor': None}

            # TTkLog.debug(f"{token=} {style=}")
            color = TTkColor.RST
            if style['color']:
                color += TTkColor.fg(f"#{style['color']}")
            if style['bgcolor']:
                color += TTkColor.bg(f"#{style['bgcolor']}")
            if style['bold']:
                color += TTkColor.BOLD
            if style['italic']:
                color += TTkColor.ITALIC
            if style['underline']:
                color += TTkColor.UNDERLINE
            self._highlightStyles[token] = color

    def setDl(self,dl):
        self._dl = dl

    def setDefaultColor(self, color:TTkColor) -> None:
        self._defaultColor = color

    def format(self, tokensource, _):
        multiline = False
        multilineId = 0
        for ttype, value in tokensource:
            if ttype == Error and self._dl.error is None:
                self._dl.error = len(self._dl.lines)-1
            # self._dl.multiline = ttype == Comment.Multiline
            multiline = ttype == Comment.Multiline

            while ttype not in self._highlightStyles:
                ttype = ttype.parent
            # TTkLog.debug (f"{ttype=}")
            # TTkLog.debug (f"{value=}")
            color:TTkColor = self._highlightStyles[ttype]
            if not color.hasForeground():
               color += self._defaultColor

            values = value.split('\n')

            self._dl.lines[-1] += TTkString(values[0],color)
            self._dl.lines += [TTkString(t,color) for t in values[1:]]
            self._dl.block[-1] = self._blockNum
            self._dl.block += [self._blockNum]*(len(values)-1)

            # self._dl.lines += [TTkString(t) for t in value.split('\n')]

            # multiline = len(values)>1 if self._dl.lines[-1]._text == values[-1] else self._dl.multiline
            # if self._dl.lines[-1]._text == '' or not multiline:
            #     self._blockNum += 1
            #     multilineId = len(self._dl.lines)

            if multiline:
                multilineId += len(values)
            else:
                multilineId = 0
                self._blockNum += 1

        if multiline:
            self._dl.multiline = multilineId

[docs] class TextDocumentHighlight(TTkTextDocument): _linesRefreshed:int = 30 __slots__ = ( '_timerRefresh', '_blocks', '_changedContent', '_refreshContent', '_lexer', '_formatter', '_defaultForegroundColor', #Signals 'highlightUpdate') def __init__(self, **kwargs): self.highlightUpdate = pyTTkSignal() self._lexer = None self._blocks = [] self._defaultForegroundColor = TTkColor.RST # self._formatter = _TTkFormatter(style='dracula') self._formatter = _TTkFormatter(style='gruvbox-dark') super().__init__(**kwargs) self._timerRefresh = TTkTimer() self._timerRefresh.timeout.connect(self._refreshEvent) self._changedContent = (0,0,len(self._dataLines)) self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) # self.contentsChange.connect(lambda a,b,c: TTkLog.debug(f"{a=} {b=} {c=}")) self.contentsChange.connect(self._saveChangedContent) try: self._lexer = guess_lexer(self.toPlainText()) TTkLog.debug(f"Using Lexer: {self._lexer.name}") except ClassNotFound: self._lexer = special.TextLexer() self._timerRefresh.start(0.3)
[docs] @staticmethod def getStyles() -> list[str]: return sorted(get_all_styles())
[docs] @staticmethod def getLexers() -> list[str]: return sorted(list(set(b for a in get_all_lexers() for b in a[1])))
[docs] @pyTTkSlot(str) def setStyle(self, alias:str) -> None: self._formatter = formatter = _TTkFormatter(style=alias) if (color:=formatter.style.background_color) and color != "#000000": self._backgroundColor = TTkColor.bg(color) else: self._backgroundColor = TTkColor.RST if self._backgroundColor == TTkColor.RST: self._defaultForegroundColor = TTkColor.RST else: r,g,b = self._backgroundColor.bgToRGB() if r+g+b < 127*3: self._defaultForegroundColor = TTkColor.WHITE else: self._defaultForegroundColor = TTkColor.BLACK TTkLog.debug(f"{color=} {alias=} {formatter.style}") self._changedContent = (0,0,len(self._dataLines)) self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) self._timerRefresh.start(0.3)
[docs] @pyTTkSlot(str) def setLexer(self, alias:str) -> None: try: self._lexer = get_lexer_by_name(alias) self._changedContent = (0,0,len(self._dataLines)) self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) self._timerRefresh.start(0.3) TTkLog.debug(f"Using Lexer: {self._lexer.name}") except ClassNotFound: self._lexer = special.TextLexer()
[docs] @pyTTkSlot(str) def guessLexerFromFilename(self, fileName:str) -> None: with open(fileName, 'r') as f: content = f.read() try: self._lexer = guess_lexer_for_filename(fileName, content) TTkLog.debug(f"Using Lexer: {self._lexer.name}") except ClassNotFound: self._lexer = special.TextLexer()
@pyTTkSlot(int,int,int) def _saveChangedContent(self,a,b,c): if self._changedContent: self._changedContent = TTkTextDocument._mergeChangesSlices(self._changedContent,(a,b,c)) else: self._changedContent = (a,b,c) if not self._refreshContent: self._refreshContent = (self._changedContent[0], TextDocumentHighlight._linesRefreshed) self._timerRefresh.start(0.1) @pyTTkSlot() def _refreshEvent(self): if not self._refreshContent: return self._acquire() ra,rb = self._refreshContent if self._changedContent: ca,cb,cc = self._changedContent self._changedContent = None self._blocks[ca:ca+cb] = [0]*cc ra = min(ra,ca) # find the beginning of the current block # TTkLog.debug(self._blocks) if ra and self._blocks: blockId = self._blocks[ra] for i,v in enumerate(reversed(self._blocks[:ra])): # TTkLog.debug(f"{i=}:{v=} {blockId=}") if v == blockId or not blockId: blockId = v ra -= 1 rb += 1 else: break # TTkLog.debug(f"{ra=} {rb=}") eof = False if (ra+rb) >= len(self._dataLines): rb = len(self._dataLines)-ra eof=True tsl = self._dataLines[ra:ra+rb] # Find the offset from the first not empty line # because pygments autostrip the heading empty lines offset = 0 for i,l in enumerate(tsl): if l != '': offset = i break # TTkLog.debug(f"Refresh {self._lexer.name} {ra=} {rb=}") tsl1 = [TTkString()]*(offset+1) block = [0]*(offset+1) kfd = _TTkFormatter.Data(tsl1, block) self._formatter.setDl(kfd) self._formatter.setDefaultColor(self._defaultForegroundColor) rawl = [l._text for l in tsl[offset:]] rawt = '\n'.join(rawl) highlight(rawt, self._lexer, self._formatter) # for ll in tsl: # TTkLog.debug(f"1: -{ll}-") # for ll in tsl1: # TTkLog.debug(f"2: -{ll}-") tsl1 = tsl1[:rb] block = block[:rb] self._dataLines[ra:ra+rb] = tsl1 + tsl[len(tsl1):] self._blocks[ra:ra+rb] = block + [-1]*(rb-len(block)) # TTkLog.debug(self._blocks) if kfd.error is not None: self._refreshContent = (ra+kfd.error,rb<<1) # TTkLog.debug(f"Error: {self._refreshContent=}") elif kfd.multiline is not None: self._refreshContent = (ra+kfd.multiline,rb<<1) elif (ra+rb) < len(self._dataLines): self._refreshContent = (ra+rb,TextDocumentHighlight._linesRefreshed) else: self._refreshContent = None # TTkLog.debug(f"{self._refreshContent=}") if not eof: self._timerRefresh.start(0.03) else: TTkLog.debug(f"Refresh {self._lexer.name} DONE!!!") self._release() self.highlightUpdate.emit() self.formatChanged.emit()