Last active
April 19, 2021 17:32
-
-
Save pryrt/5ade1a13501c4df47f2fd8c00f1c7b03 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
''' | |
modified from https://github.com/Ekopalypse/NppPythonScripts/blob/master/npp/EnhanceAnyLexer.py | |
Adds a special "regex" notation of "PCJ:CSSCOLOR" which will change the coloring rules: | |
if you use that "regex", it will actually match r'\#[[:xdigit:]]{6}' and grab the color from | |
the RGB value. | |
available in gist at https://gist.github.com/pryrt/5ade1a13501c4df47f2fd8c00f1c7b03 | |
Provides additional color options and should be used in conjunction with | |
either an built-in or an user defined lexer. | |
An indicator is used to avoid style collisions. | |
Although the Scintilla documentation states that indicators 0-7 are reserved for the lexers, | |
indicator 0 is used. Change self.INDICATOR_ID if there is a problem. | |
Even when using more than one regex, it is not necessary to define more than one indicator | |
because the class uses the flag SC_INDICFLAG_VALUEFORE. | |
See https://www.scintilla.org/ScintillaDoc.html#Indicators for more information on that topic | |
''' | |
import sys | |
import re | |
from Npp import (notepad, editor, editor1, editor2, | |
NOTIFICATION, SCINTILLANOTIFICATION, | |
INDICATORSTYLE, INDICFLAG, INDICVALUE) | |
if sys.version_info[0] == 2: | |
from collections import OrderedDict as _dict | |
else: | |
_dict = dict | |
class EnhanceLexer: | |
def __init__(self): | |
''' | |
Initialize the class, should be called once only. | |
''' | |
current_version = notepad.getPluginVersion() | |
if current_version < '1.5.4.0': | |
notepad.messageBox('It is needed to run PythonScript version 1.5.4.0 or higher', | |
'Unsupported PythonScript verion: {}'.format(current_version)) | |
return | |
self.INDICATOR_ID = 0 | |
self.registered_lexers = _dict() | |
self.document_is_of_interest = False | |
self.regexes = None | |
self.excluded_styles = None | |
editor1.indicSetStyle(self.INDICATOR_ID, INDICATORSTYLE.TEXTFORE) | |
editor1.indicSetFlags(self.INDICATOR_ID, INDICFLAG.VALUEFORE) | |
editor2.indicSetStyle(self.INDICATOR_ID, INDICATORSTYLE.TEXTFORE) | |
editor2.indicSetFlags(self.INDICATOR_ID, INDICFLAG.VALUEFORE) | |
editor.callbackSync(self.on_updateui, [SCINTILLANOTIFICATION.UPDATEUI]) | |
editor.callbackSync(self.on_marginclick, [SCINTILLANOTIFICATION.MARGINCLICK]) | |
notepad.callback(self.on_langchanged, [NOTIFICATION.LANGCHANGED]) | |
notepad.callback(self.on_bufferactivated, [NOTIFICATION.BUFFERACTIVATED]) | |
@staticmethod | |
def rgb(r, g, b): | |
''' | |
Helper function | |
Retrieves rgb color triple and converts it | |
into its integer representation | |
Args: | |
r = integer, red color value in range of 0-255 | |
g = integer, green color value in range of 0-255 | |
b = integer, blue color value in range of 0-255 | |
Returns: | |
integer | |
''' | |
return (b << 16) + (g << 8) + r | |
def register_lexer(self, lexer_name, _regexes, excluded_styles): | |
''' | |
reformat provided regexes and cache everything | |
within registered_lexers dictionary. | |
Args: | |
lexer_name = string, expected values as returned by notepad.getLanguageName | |
without the "udf - " if it is an user defined language | |
_regexes = dict, in the form of | |
_regexes[(int, (r, g, b))] = (r'', [int]) | |
excluded_styles = list of integers | |
Returns: | |
None | |
''' | |
regexes = _dict() | |
for k, v in _regexes.items(): | |
console.write("register<lexer:{}, k:{},v:{}>\n".format(lexer_name.lower(),k,v)) | |
regexes[(k[0], self.rgb(*k[1]) | INDICVALUE.BIT)] = v | |
self.registered_lexers[lexer_name.lower()] = (regexes, excluded_styles) | |
def paint_it(self, color, match_position, length, start_position, end_position): | |
''' | |
This is where the actual coloring takes place. | |
Color, the position of the first character and | |
the length of the text to be colored must be provided. | |
Coloring occurs only if the character at the current position | |
has not a style from the excluded styles list assigned. | |
Args: | |
color = integer, expected in range of 0-16777215 | |
match_position = integer, denotes the start position of a match | |
length = integer, denotes how many chars need to be colored. | |
start_position = integer, denotes the start position of the visual area | |
end_position = integer, denotes the end position of the visual area | |
Returns: | |
None | |
''' | |
if (match_position + length < start_position or | |
match_position > end_position or | |
editor.getStyleAt(match_position) in self.excluded_styles): | |
return | |
editor.setIndicatorCurrent(0) | |
editor.setIndicatorValue(color) | |
editor.indicatorFillRange(match_position, length) | |
def style(self): | |
''' | |
Calculates the text area to be searched for in the current document. | |
Calls up the regexes to find the position and | |
calculates the length of the text to be colored. | |
Deletes the old indicators before setting new ones. | |
Args: | |
None | |
Returns: | |
None | |
''' | |
start_line = editor.docLineFromVisible(editor.getFirstVisibleLine()) | |
end_line = editor.docLineFromVisible(start_line + editor.linesOnScreen()) | |
if editor.getWrapMode(): | |
end_line = sum([editor.wrapCount(x) for x in range(end_line)]) | |
onscreen_start_position = editor.positionFromLine(start_line) | |
onscreen_end_pos = editor.getLineEndPosition(end_line) | |
editor.setIndicatorCurrent(0) | |
editor.indicatorClearRange(0, editor.getTextLength()) | |
for color, regex in self.regexes.items(): | |
console.write("style<color:{}, regex:{}>\n".format(color,regex)) | |
if regex[0] == 'PCJ:CSSCOLOR': | |
def notalambda(m): | |
r = int(m.group(regex[1])[1:3], base=16) | |
g = int(m.group(regex[1])[3:5], base=16) | |
b = int(m.group(regex[1])[5:7], base=16) | |
#r,g,b = 127,127,127 | |
my_rgb = self.rgb(r,g,b) | INDICVALUE.BIT | |
console.write("calling special CSSCOLOR: groupN:'{}' {}..{}:\t{} {} {}: {}\n".format(m.group(regex[1]), m.start(regex[1]), m.end(regex[1]), r, g, b, my_rgb)) | |
self.paint_it(my_rgb, | |
m.span(regex[1])[0], | |
m.span(regex[1])[1] - m.span(regex[1])[0], | |
onscreen_start_position, | |
onscreen_end_pos) | |
editor.research(r'\#[[:xdigit:]]{6}\b', | |
notalambda, | |
0, | |
onscreen_start_position, | |
onscreen_end_pos) | |
else: | |
editor.research(regex[0], | |
lambda m: self.paint_it(color[1], | |
m.span(regex[1])[0], | |
m.span(regex[1])[1] - m.span(regex[1])[0], | |
onscreen_start_position, | |
onscreen_end_pos), | |
0, | |
onscreen_start_position, | |
onscreen_end_pos) | |
def check_lexers(self): | |
''' | |
Checks if the current document of each view is of interest | |
and sets the flag accordingly | |
Args: | |
None | |
Returns: | |
None | |
''' | |
current_language = notepad.getLanguageName(notepad.getLangType()).replace('udf - ','').lower() | |
self.document_is_of_interest = current_language in self.registered_lexers | |
if self.document_is_of_interest: | |
self.regexes, self.excluded_styles = self.registered_lexers[current_language] | |
def on_marginclick(self, args): | |
''' | |
Callback which gets called every time one clicks the symbol margin. | |
Triggers the styling function if the document is of interest. | |
Args: | |
margin, only the symbol marign (=2) is of interest | |
Returns: | |
None | |
''' | |
if args['margin'] == 2 and self.document_is_of_interest : | |
self.style() | |
def on_bufferactivated(self, args): | |
''' | |
Callback which gets called every time one switches a document. | |
Triggers the check if the document is of interest. | |
Args: | |
provided by notepad object but none are of interest | |
Returns: | |
None | |
''' | |
self.check_lexers() | |
def on_updateui(self, args): | |
''' | |
Callback which gets called every time scintilla | |
(aka the editor) changed something within the document. | |
Triggers the styling function if the document is of interest. | |
Args: | |
provided by scintilla but none are of interest | |
Returns: | |
None | |
''' | |
if self.document_is_of_interest: | |
self.style() | |
def on_langchanged(self, args): | |
''' | |
Callback gets called every time one uses the Language menu to set a lexer | |
Triggers the check if the document is of interest | |
Args: | |
provided by notepad object but none are of interest | |
Returns: | |
None | |
''' | |
self.check_lexers() | |
def main(self): | |
''' | |
Main function entry point. | |
Simulates two events to enforce detection of current document | |
and potential styling. | |
Args: | |
None | |
Returns: | |
None | |
''' | |
self.on_bufferactivated(None) | |
self.on_updateui(None) | |
# Usage: | |
# | |
# Only the active document and for performance reasons, only the currently visible area | |
# is scanned and colored. | |
# This means, that a regular expression match is assumed to reflect only one line of code | |
# and not to extend over multiple lines. | |
# As an illustration, in python one can define, for example, a function like this | |
# | |
# def my_function(param1, param2, param3, param4): | |
# pass | |
# | |
# but it is also valid to define it like this | |
# | |
# def my_function(param1, | |
# param2, | |
# param3, | |
# param4): | |
# pass | |
# | |
# Now, if a regular expression like "(?:(?:def)\s\w+)\s*\((.+)\):" were used to color all parameters, | |
# then this would only work as long as the line "def my_function(param1," is visible. | |
# | |
# A possible approach to avoid this would be to define an offset range. | |
# | |
# offset_start_line = start_line - offset | |
# if offset_start_line < 0 then offset_start_line = 0 | |
# | |
# Not sure if this is the best approach - still investigating. | |
# | |
# Definition of colors and regular expressions | |
# Note, the order in which a regular expressions will be processed is determined by its creation, | |
# that is, the first definition is processed first, then the 2nd, and so on | |
# | |
# The basic structure always looks like this | |
# | |
# regexes[(a, b)] = (c, d) | |
# | |
# | |
# regexes = an ordered dictionary which ensures that the regular expressions | |
# are always processed in the same order. | |
# a = an unique number - suggestion, start with 0 and always increase by one (per lexer) | |
# b = color tuple in the form of (r,g,b). Example (255,0,0) for the color red. | |
# c = raw byte string, describes the regular expression. Example r'\w+' | |
# d = integer, denotes which match group should be considered | |
# Example | |
# builtin lexers - like python | |
py_regexes = _dict() | |
# cls and self objects - return match 0 | |
py_regexes[(0, (224, 108, 117))] = (r'\b(cls|self)\b', 0) | |
# function parameters - return match 1 | |
py_regexes[(1, (209, 154, 102))] = (r'(?:(?:def)\s\w+)\s*\((.+)\):', 1) | |
# args and kwargs - return match 0 | |
py_regexes[(2, (86, 182, 194))] = (r'(\*|\*\*)(?=\w)', 0) | |
# functions and class instances but not definitions - return match 1 | |
py_regexes[(3, (79, 175, 239))] = (r'class\s*\w+?(?=\()|def\s*\w+?(?=\()|(\w+?(?=\())', 1) | |
# dunder functions and special keywords - return match 0 | |
py_regexes[(4, (86, 182, 194))] = (r'\b(editor|editor1|editor2|notepad|console|__\w+__|super|object|type|print)\b', 0) | |
# There is no standardization in defining the style IDs of lexers attributes, | |
# hence one has to check the stylers.xml (or THEMENAME.xml) to see which | |
# IDs are defined by the respective lexer and what its purposes are to | |
# create an list of style ids which shouldn't be altered. | |
py_excluded_styles = [1, 3, 4, 6, 7, 12, 16, 17, 18, 19] | |
# user defined lexers | |
# Definition of which area should not be styled | |
# 0 = default style | |
# 1 = comment style | |
# 2 = comment line style | |
# 3 = numbers style | |
# 4 = keyword1 style | |
# ... | |
# 11 = keyword8 style | |
# 12 = operator style | |
# 13 = fold in code 1 style | |
# 14 = fold in code 2 style | |
# 15 = fold in comment style | |
# 16 = delimiter1 style | |
# ... | |
# 23 = delimiter8 style | |
# excluded_styles = [1, 2, 16, 17, 18, 19, 20, 21, 22, 23] | |
md_regexes = _dict() | |
# single underscores | |
md_regexes[(0, (224, 108, 117))] = (r'_.*?_', 0) | |
# double underscores but only the word part | |
md_regexes[(1, (209, 154, 102))] = (r'__(.*?)__', 1) | |
md_excluded_styles = [1, 2, 16, 17, 18, 19, 20, 21, 22, 23] | |
################################ | |
# my experiment: can I find six hex characters and set their color to their color value? | |
html_regexes = _dict() | |
html_regexes[(0, (0,0,0))] = (r'PCJ:CSSCOLOR', 0) | |
_enhance_lexer = EnhanceLexer() | |
#_enhance_lexer.register_lexer('python', py_regexes, py_excluded_styles) | |
#_enhance_lexer.register_lexer('Markdown (Default)', md_regexes, md_excluded_styles) | |
_enhance_lexer.register_lexer('HTML', html_regexes, py_excluded_styles) | |
# start | |
_enhance_lexer.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment