# Does Python source formatting for Scintilla controls. import array import string import win32api import win32con import win32ui from . import scintillacon WM_KICKIDLE = 0x036A # Used to indicate that style should use default color from win32con import CLR_INVALID debugging = 0 if debugging: # Output must go to another process else the result of # the printing itself will trigger again trigger a trace. import win32trace import win32traceutil def trace(*args): win32trace.write(" ".join(map(str, args)) + "\n") else: trace = lambda *args: None class Style: """Represents a single format""" def __init__(self, name, format, background=CLR_INVALID): self.name = name # Name the format representes eg, "String", "Class" # Default background for each style is only used when there are no # saved settings (generally on first startup) self.background = self.default_background = background if type(format) == type(""): self.aliased = format self.format = None else: self.format = format self.aliased = None self.stylenum = None # Not yet registered. def IsBasedOnDefault(self): return len(self.format) == 5 # If the currently extended font defintion matches the # default format, restore the format to the "simple" format. def NormalizeAgainstDefault(self, defaultFormat): if self.IsBasedOnDefault(): return 0 # No more to do, and not changed. bIsDefault = ( self.format[7] == defaultFormat[7] and self.format[2] == defaultFormat[2] ) if bIsDefault: self.ForceAgainstDefault() return bIsDefault def ForceAgainstDefault(self): self.format = self.format[:5] def GetCompleteFormat(self, defaultFormat): # Get the complete style after applying any relevant defaults. if len(self.format) == 5: # It is a default one fmt = self.format + defaultFormat[5:] else: fmt = self.format flags = ( win32con.CFM_BOLD | win32con.CFM_CHARSET | win32con.CFM_COLOR | win32con.CFM_FACE | win32con.CFM_ITALIC | win32con.CFM_SIZE ) return (flags,) + fmt[1:] # The Formatter interface # used primarily when the actual formatting is done by Scintilla! class FormatterBase: def __init__(self, scintilla): self.scintilla = scintilla self.baseFormatFixed = (-402653169, 0, 200, 0, 0, 0, 49, "Courier New") self.baseFormatProp = (-402653169, 0, 200, 0, 0, 0, 49, "Arial") self.bUseFixed = 1 self.styles = {} # Indexed by name self.styles_by_id = {} # Indexed by allocated ID. self.SetStyles() def HookFormatter(self, parent=None): raise NotImplementedError() # Used by the IDLE extensions to quickly determine if a character is a string. def GetStringStyle(self, pos): try: style = self.styles_by_id[self.scintilla.SCIGetStyleAt(pos)] except KeyError: # A style we dont know about - probably not even a .py file - can't be a string return None if style.name in self.string_style_names: return style return None def RegisterStyle(self, style, stylenum): assert stylenum is not None, "We must have a style number" assert style.stylenum is None, "Style has already been registered" assert stylenum not in self.styles, "We are reusing a style number!" style.stylenum = stylenum self.styles[style.name] = style self.styles_by_id[stylenum] = style def SetStyles(self): raise NotImplementedError() def GetSampleText(self): return "Sample Text for the Format Dialog" def GetDefaultFormat(self): if self.bUseFixed: return self.baseFormatFixed return self.baseFormatProp # Update the control with the new style format. def _ReformatStyle(self, style): ## Selection (background only for now) ## Passing False for WPARAM to SCI_SETSELBACK is documented as resetting to scintilla default, ## but does not work - selection background is not visible at all. ## Default value in SPECIAL_STYLES taken from scintilla source. if style.name == STYLE_SELECTION: clr = style.background self.scintilla.SendScintilla(scintillacon.SCI_SETSELBACK, True, clr) ## Can't change font for selection, but could set color ## However, the font color dropbox has no option for default, and thus would ## always override syntax coloring ## clr = style.format[4] ## self.scintilla.SendScintilla(scintillacon.SCI_SETSELFORE, clr != CLR_INVALID, clr) return assert style.stylenum is not None, "Unregistered style." # print "Reformat style", style.name, style.stylenum scintilla = self.scintilla stylenum = style.stylenum # Now we have the style number, indirect for the actual style. if style.aliased is not None: style = self.styles[style.aliased] f = style.format if style.IsBasedOnDefault(): baseFormat = self.GetDefaultFormat() else: baseFormat = f scintilla.SCIStyleSetFore(stylenum, f[4]) scintilla.SCIStyleSetFont(stylenum, baseFormat[7], baseFormat[5]) if f[1] & 1: scintilla.SCIStyleSetBold(stylenum, 1) else: scintilla.SCIStyleSetBold(stylenum, 0) if f[1] & 2: scintilla.SCIStyleSetItalic(stylenum, 1) else: scintilla.SCIStyleSetItalic(stylenum, 0) scintilla.SCIStyleSetSize(stylenum, int(baseFormat[2] / 20)) scintilla.SCIStyleSetEOLFilled(stylenum, 1) # Only needed for unclosed strings. ## Default style background to whitespace background if set, ## otherwise use system window color bg = style.background if bg == CLR_INVALID: bg = self.styles[STYLE_DEFAULT].background if bg == CLR_INVALID: bg = win32api.GetSysColor(win32con.COLOR_WINDOW) scintilla.SCIStyleSetBack(stylenum, bg) def GetStyleByNum(self, stylenum): return self.styles_by_id[stylenum] def ApplyFormattingStyles(self, bReload=1): if bReload: self.LoadPreferences() baseFormat = self.GetDefaultFormat() defaultStyle = Style("default", baseFormat) defaultStyle.stylenum = scintillacon.STYLE_DEFAULT self._ReformatStyle(defaultStyle) for style in list(self.styles.values()): if style.aliased is None: style.NormalizeAgainstDefault(baseFormat) self._ReformatStyle(style) self.scintilla.InvalidateRect() # Some functions for loading and saving preferences. By default # an INI file (well, MFC maps this to the registry) is used. def LoadPreferences(self): self.baseFormatFixed = eval( self.LoadPreference("Base Format Fixed", str(self.baseFormatFixed)) ) self.baseFormatProp = eval( self.LoadPreference("Base Format Proportional", str(self.baseFormatProp)) ) self.bUseFixed = int(self.LoadPreference("Use Fixed", 1)) for style in list(self.styles.values()): new = self.LoadPreference(style.name, str(style.format)) try: style.format = eval(new) except: print("Error loading style data for", style.name) # Use "vanilla" background hardcoded in PYTHON_STYLES if no settings in registry style.background = int( self.LoadPreference( style.name + " background", style.default_background ) ) def LoadPreference(self, name, default): return win32ui.GetProfileVal("Format", name, default) def SavePreferences(self): self.SavePreference("Base Format Fixed", str(self.baseFormatFixed)) self.SavePreference("Base Format Proportional", str(self.baseFormatProp)) self.SavePreference("Use Fixed", self.bUseFixed) for style in list(self.styles.values()): if style.aliased is None: self.SavePreference(style.name, str(style.format)) bg_name = style.name + " background" self.SavePreference(bg_name, style.background) def SavePreference(self, name, value): win32ui.WriteProfileVal("Format", name, value) # An abstract formatter # For all formatters we actually implement here. # (as opposed to those formatters built in to Scintilla) class Formatter(FormatterBase): def __init__(self, scintilla): self.bCompleteWhileIdle = 0 self.bHaveIdleHandler = 0 # Dont currently have an idle handle self.nextstylenum = 0 FormatterBase.__init__(self, scintilla) def HookFormatter(self, parent=None): if parent is None: parent = self.scintilla.GetParent() # was GetParentFrame()!? parent.HookNotify(self.OnStyleNeeded, scintillacon.SCN_STYLENEEDED) def OnStyleNeeded(self, std, extra): notify = self.scintilla.SCIUnpackNotifyMessage(extra) endStyledChar = self.scintilla.SendScintilla(scintillacon.SCI_GETENDSTYLED) lineEndStyled = self.scintilla.LineFromChar(endStyledChar) endStyled = self.scintilla.LineIndex(lineEndStyled) # print "enPosPaint %d endStyledChar %d lineEndStyled %d endStyled %d" % (endPosPaint, endStyledChar, lineEndStyled, endStyled) self.Colorize(endStyled, notify.position) def ColorSeg(self, start, end, styleName): end = end + 1 # assert end-start>=0, "Can't have negative styling" stylenum = self.styles[styleName].stylenum while start < end: self.style_buffer[start] = stylenum start = start + 1 # self.scintilla.SCISetStyling(end - start + 1, stylenum) def RegisterStyle(self, style, stylenum=None): if stylenum is None: stylenum = self.nextstylenum self.nextstylenum = self.nextstylenum + 1 FormatterBase.RegisterStyle(self, style, stylenum) def ColorizeString(self, str, charStart, styleStart): raise RuntimeError("You must override this method") def Colorize(self, start=0, end=-1): scintilla = self.scintilla # scintilla's formatting is all done in terms of utf, so # we work with utf8 bytes instead of unicode. This magically # works as any extended chars found in the utf8 don't change # the semantics. stringVal = scintilla.GetTextRange(start, end, decode=False) if start > 0: stylenum = scintilla.SCIGetStyleAt(start - 1) styleStart = self.GetStyleByNum(stylenum).name else: styleStart = None # trace("Coloring", start, end, end-start, len(stringVal), styleStart, self.scintilla.SCIGetCharAt(start)) scintilla.SCIStartStyling(start, 31) self.style_buffer = array.array("b", (0,) * len(stringVal)) self.ColorizeString(stringVal, styleStart) scintilla.SCISetStylingEx(self.style_buffer) self.style_buffer = None # trace("After styling, end styled is", self.scintilla.SCIGetEndStyled()) if ( self.bCompleteWhileIdle and not self.bHaveIdleHandler and end != -1 and end < scintilla.GetTextLength() ): self.bHaveIdleHandler = 1 win32ui.GetApp().AddIdleHandler(self.DoMoreColoring) # Kicking idle makes the app seem slower when initially repainting! # win32ui.GetMainFrame().PostMessage(WM_KICKIDLE, 0, 0) def DoMoreColoring(self, handler, count): try: scintilla = self.scintilla endStyled = scintilla.SCIGetEndStyled() lineStartStyled = scintilla.LineFromChar(endStyled) start = scintilla.LineIndex(lineStartStyled) end = scintilla.LineIndex(lineStartStyled + 1) textlen = scintilla.GetTextLength() if end < 0: end = textlen finished = end >= textlen self.Colorize(start, end) except (win32ui.error, AttributeError): # Window may have closed before we finished - no big deal! finished = 1 if finished: self.bHaveIdleHandler = 0 win32ui.GetApp().DeleteIdleHandler(handler) return not finished # A Formatter that knows how to format Python source from keyword import iskeyword, kwlist wordstarts = "_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" wordchars = "._0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" operators = "%^&*()-+=|{}[]:;<>,/?!.~" STYLE_DEFAULT = "Whitespace" STYLE_COMMENT = "Comment" STYLE_COMMENT_BLOCK = "Comment Blocks" STYLE_NUMBER = "Number" STYLE_STRING = "String" STYLE_SQSTRING = "SQ String" STYLE_TQSSTRING = "TQS String" STYLE_TQDSTRING = "TQD String" STYLE_KEYWORD = "Keyword" STYLE_CLASS = "Class" STYLE_METHOD = "Method" STYLE_OPERATOR = "Operator" STYLE_IDENTIFIER = "Identifier" STYLE_BRACE = "Brace/Paren - matching" STYLE_BRACEBAD = "Brace/Paren - unmatched" STYLE_STRINGEOL = "String with no terminator" STYLE_LINENUMBER = "Line numbers" STYLE_INDENTGUIDE = "Indent guide" STYLE_SELECTION = "Selection" STRING_STYLES = [ STYLE_STRING, STYLE_SQSTRING, STYLE_TQSSTRING, STYLE_TQDSTRING, STYLE_STRINGEOL, ] # These styles can have any ID - they are not special to scintilla itself. # However, if we use the built-in lexer, then we must use its style numbers # so in that case, they _are_ special. # (name, format, background, scintilla id) PYTHON_STYLES = [ (STYLE_DEFAULT, (0, 0, 200, 0, 0x808080), CLR_INVALID, scintillacon.SCE_P_DEFAULT), ( STYLE_COMMENT, (0, 2, 200, 0, 0x008000), CLR_INVALID, scintillacon.SCE_P_COMMENTLINE, ), ( STYLE_COMMENT_BLOCK, (0, 2, 200, 0, 0x808080), CLR_INVALID, scintillacon.SCE_P_COMMENTBLOCK, ), (STYLE_NUMBER, (0, 0, 200, 0, 0x808000), CLR_INVALID, scintillacon.SCE_P_NUMBER), (STYLE_STRING, (0, 0, 200, 0, 0x008080), CLR_INVALID, scintillacon.SCE_P_STRING), (STYLE_SQSTRING, STYLE_STRING, CLR_INVALID, scintillacon.SCE_P_CHARACTER), (STYLE_TQSSTRING, STYLE_STRING, CLR_INVALID, scintillacon.SCE_P_TRIPLE), (STYLE_TQDSTRING, STYLE_STRING, CLR_INVALID, scintillacon.SCE_P_TRIPLEDOUBLE), (STYLE_STRINGEOL, (0, 0, 200, 0, 0x000000), 0x008080, scintillacon.SCE_P_STRINGEOL), (STYLE_KEYWORD, (0, 1, 200, 0, 0x800000), CLR_INVALID, scintillacon.SCE_P_WORD), (STYLE_CLASS, (0, 1, 200, 0, 0xFF0000), CLR_INVALID, scintillacon.SCE_P_CLASSNAME), (STYLE_METHOD, (0, 1, 200, 0, 0x808000), CLR_INVALID, scintillacon.SCE_P_DEFNAME), ( STYLE_OPERATOR, (0, 0, 200, 0, 0x000000), CLR_INVALID, scintillacon.SCE_P_OPERATOR, ), ( STYLE_IDENTIFIER, (0, 0, 200, 0, 0x000000), CLR_INVALID, scintillacon.SCE_P_IDENTIFIER, ), ] # These styles _always_ have this specific style number, regardless of # internal or external formatter. SPECIAL_STYLES = [ (STYLE_BRACE, (0, 0, 200, 0, 0x000000), 0xFFFF80, scintillacon.STYLE_BRACELIGHT), (STYLE_BRACEBAD, (0, 0, 200, 0, 0x000000), 0x8EA5F2, scintillacon.STYLE_BRACEBAD), ( STYLE_LINENUMBER, (0, 0, 200, 0, 0x000000), win32api.GetSysColor(win32con.COLOR_3DFACE), scintillacon.STYLE_LINENUMBER, ), ( STYLE_INDENTGUIDE, (0, 0, 200, 0, 0x000000), CLR_INVALID, scintillacon.STYLE_INDENTGUIDE, ), ## Not actually a style; requires special handling to send appropriate messages to scintilla ( STYLE_SELECTION, (0, 0, 200, 0, CLR_INVALID), win32api.RGB(0xC0, 0xC0, 0xC0), 999999, ), ] PythonSampleCode = """\ # Some Python class Sample(Super): def Fn(self): \tself.v = 1024 dest = 'dest.html' x = func(a + 1)|) s = "I forget... ## A large ## comment block""" class PythonSourceFormatter(Formatter): string_style_names = STRING_STYLES def GetSampleText(self): return PythonSampleCode def LoadStyles(self): pass def SetStyles(self): for name, format, bg, ignore in PYTHON_STYLES: self.RegisterStyle(Style(name, format, bg)) for name, format, bg, sc_id in SPECIAL_STYLES: self.RegisterStyle(Style(name, format, bg), sc_id) def ClassifyWord(self, cdoc, start, end, prevWord): word = cdoc[start : end + 1].decode("latin-1") attr = STYLE_IDENTIFIER if prevWord == "class": attr = STYLE_CLASS elif prevWord == "def": attr = STYLE_METHOD elif word[0] in string.digits: attr = STYLE_NUMBER elif iskeyword(word): attr = STYLE_KEYWORD self.ColorSeg(start, end, attr) return word def ColorizeString(self, str, styleStart): if styleStart is None: styleStart = STYLE_DEFAULT return self.ColorizePythonCode(str, 0, styleStart) def ColorizePythonCode(self, cdoc, charStart, styleStart): # Straight translation of C++, should do better lengthDoc = len(cdoc) if lengthDoc <= charStart: return prevWord = "" state = styleStart chPrev = chPrev2 = chPrev3 = " " chNext2 = chNext = cdoc[charStart : charStart + 1].decode("latin-1") startSeg = i = charStart while i < lengthDoc: ch = chNext chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1 : i + 2].decode("latin-1") chNext2 = " " if i + 2 < lengthDoc: chNext2 = cdoc[i + 2 : i + 3].decode("latin-1") if state == STYLE_DEFAULT: if ch in wordstarts: self.ColorSeg(startSeg, i - 1, STYLE_DEFAULT) state = STYLE_KEYWORD startSeg = i elif ch == "#": self.ColorSeg(startSeg, i - 1, STYLE_DEFAULT) if chNext == "#": state = STYLE_COMMENT_BLOCK else: state = STYLE_COMMENT startSeg = i elif ch == '"': self.ColorSeg(startSeg, i - 1, STYLE_DEFAULT) startSeg = i state = STYLE_COMMENT if chNext == '"' and chNext2 == '"': i = i + 2 state = STYLE_TQDSTRING ch = " " chPrev = " " chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] else: state = STYLE_STRING elif ch == "'": self.ColorSeg(startSeg, i - 1, STYLE_DEFAULT) startSeg = i state = STYLE_COMMENT if chNext == "'" and chNext2 == "'": i = i + 2 state = STYLE_TQSSTRING ch = " " chPrev = " " chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] else: state = STYLE_SQSTRING elif ch in operators: self.ColorSeg(startSeg, i - 1, STYLE_DEFAULT) self.ColorSeg(i, i, STYLE_OPERATOR) startSeg = i + 1 elif state == STYLE_KEYWORD: if ch not in wordchars: prevWord = self.ClassifyWord(cdoc, startSeg, i - 1, prevWord) state = STYLE_DEFAULT startSeg = i if ch == "#": if chNext == "#": state = STYLE_COMMENT_BLOCK else: state = STYLE_COMMENT elif ch == '"': if chNext == '"' and chNext2 == '"': i = i + 2 state = STYLE_TQDSTRING ch = " " chPrev = " " chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] else: state = STYLE_STRING elif ch == "'": if chNext == "'" and chNext2 == "'": i = i + 2 state = STYLE_TQSSTRING ch = " " chPrev = " " chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] else: state = STYLE_SQSTRING elif ch in operators: self.ColorSeg(startSeg, i, STYLE_OPERATOR) startSeg = i + 1 elif state == STYLE_COMMENT or state == STYLE_COMMENT_BLOCK: if ch == "\r" or ch == "\n": self.ColorSeg(startSeg, i - 1, state) state = STYLE_DEFAULT startSeg = i elif state == STYLE_STRING: if ch == "\\": if chNext == '"' or chNext == "'" or chNext == "\\": i = i + 1 ch = chNext chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] elif ch == '"': self.ColorSeg(startSeg, i, STYLE_STRING) state = STYLE_DEFAULT startSeg = i + 1 elif state == STYLE_SQSTRING: if ch == "\\": if chNext == '"' or chNext == "'" or chNext == "\\": i = i + 1 ch = chNext chNext = " " if i + 1 < lengthDoc: chNext = cdoc[i + 1] elif ch == "'": self.ColorSeg(startSeg, i, STYLE_SQSTRING) state = STYLE_DEFAULT startSeg = i + 1 elif state == STYLE_TQSSTRING: if ch == "'" and chPrev == "'" and chPrev2 == "'" and chPrev3 != "\\": self.ColorSeg(startSeg, i, STYLE_TQSSTRING) state = STYLE_DEFAULT startSeg = i + 1 elif ( state == STYLE_TQDSTRING and ch == '"' and chPrev == '"' and chPrev2 == '"' and chPrev3 != "\\" ): self.ColorSeg(startSeg, i, STYLE_TQDSTRING) state = STYLE_DEFAULT startSeg = i + 1 chPrev3 = chPrev2 chPrev2 = chPrev chPrev = ch i = i + 1 if startSeg < lengthDoc: if state == STYLE_KEYWORD: self.ClassifyWord(cdoc, startSeg, lengthDoc - 1, prevWord) else: self.ColorSeg(startSeg, lengthDoc - 1, state) # These taken from the SciTE properties file. source_formatter_extensions = [ (".py .pys .pyw".split(), scintillacon.SCLEX_PYTHON), (".html .htm .asp .shtml".split(), scintillacon.SCLEX_HTML), ( "c .cc .cpp .cxx .h .hh .hpp .hxx .idl .odl .php3 .phtml .inc .js".split(), scintillacon.SCLEX_CPP, ), (".vbs .frm .ctl .cls".split(), scintillacon.SCLEX_VB), (".pl .pm .cgi .pod".split(), scintillacon.SCLEX_PERL), (".sql .spec .body .sps .spb .sf .sp".split(), scintillacon.SCLEX_SQL), (".tex .sty".split(), scintillacon.SCLEX_LATEX), (".xml .xul".split(), scintillacon.SCLEX_XML), (".err".split(), scintillacon.SCLEX_ERRORLIST), (".mak".split(), scintillacon.SCLEX_MAKEFILE), (".bat .cmd".split(), scintillacon.SCLEX_BATCH), ] class BuiltinSourceFormatter(FormatterBase): # A class that represents a formatter built-in to Scintilla def __init__(self, scintilla, ext): self.ext = ext FormatterBase.__init__(self, scintilla) def Colorize(self, start=0, end=-1): self.scintilla.SendScintilla(scintillacon.SCI_COLOURISE, start, end) def RegisterStyle(self, style, stylenum=None): assert style.stylenum is None, "Style has already been registered" if stylenum is None: stylenum = self.nextstylenum self.nextstylenum = self.nextstylenum + 1 assert self.styles.get(stylenum) is None, "We are reusing a style number!" style.stylenum = stylenum self.styles[style.name] = style self.styles_by_id[stylenum] = style def HookFormatter(self, parent=None): sc = self.scintilla for exts, formatter in source_formatter_extensions: if self.ext in exts: formatter_use = formatter break else: formatter_use = scintillacon.SCLEX_PYTHON sc.SendScintilla(scintillacon.SCI_SETLEXER, formatter_use) keywords = " ".join(kwlist) sc.SCISetKeywords(keywords) class BuiltinPythonSourceFormatter(BuiltinSourceFormatter): sci_lexer_name = scintillacon.SCLEX_PYTHON string_style_names = STRING_STYLES def __init__(self, sc, ext=".py"): BuiltinSourceFormatter.__init__(self, sc, ext) def SetStyles(self): for name, format, bg, sc_id in PYTHON_STYLES: self.RegisterStyle(Style(name, format, bg), sc_id) for name, format, bg, sc_id in SPECIAL_STYLES: self.RegisterStyle(Style(name, format, bg), sc_id) def GetSampleText(self): return PythonSampleCode