editor: Color the parts list with the theme
- Can change theme while parts list is showing.
- Added more themes.
- Added a botom dividing line to the parts list.
- Parts aren't always classes or functions.
- Also tidied up theme related code
- Always using Code class and the TextLexer if theres no lexer
for the file.
- Not nesting parse_rgb and charstyle_for_token_type.
This commit is contained in:
parent
04aeacac14
commit
7138a3c08c
2 changed files with 82 additions and 93 deletions
|
|
@ -15,6 +15,7 @@ import fill3.terminal as terminal
|
||||||
import lscolors
|
import lscolors
|
||||||
import pygments
|
import pygments
|
||||||
import pygments.lexers
|
import pygments.lexers
|
||||||
|
import pygments.lexers.special
|
||||||
import pygments.styles
|
import pygments.styles
|
||||||
import termstr
|
import termstr
|
||||||
|
|
||||||
|
|
@ -38,32 +39,35 @@ def highlight_line(line):
|
||||||
NATIVE_STYLE = pygments.styles.get_style_by_name("paraiso-dark")
|
NATIVE_STYLE = pygments.styles.get_style_by_name("paraiso-dark")
|
||||||
|
|
||||||
|
|
||||||
def _syntax_highlight(text, lexer, style):
|
|
||||||
@functools.lru_cache(maxsize=500)
|
@functools.lru_cache(maxsize=500)
|
||||||
def _parse_rgb(hex_rgb):
|
def parse_rgb(hex_rgb):
|
||||||
if hex_rgb.startswith("#"):
|
if hex_rgb.startswith("#"):
|
||||||
hex_rgb = hex_rgb[1:]
|
hex_rgb = hex_rgb[1:]
|
||||||
return tuple(int("0x" + hex_rgb[index:index+2], base=16) for index in [0, 2, 4])
|
return tuple(int("0x" + hex_rgb[index:index+2], base=16) for index in [0, 2, 4])
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=500)
|
@functools.lru_cache(maxsize=500)
|
||||||
def _char_style_for_token_type(token_type, default_bg_color, default_style):
|
def char_style_for_token_type(token_type, style):
|
||||||
|
default_bg_color = parse_rgb(style.background_color)
|
||||||
|
default_style = termstr.CharStyle(bg_color=default_bg_color)
|
||||||
try:
|
try:
|
||||||
token_style = style.style_for_token(token_type)
|
token_style = style.style_for_token(token_type)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default_style
|
return default_style
|
||||||
fg_color = (termstr.Color.black if token_style["color"] is None
|
fg_color = (termstr.Color.black if token_style["color"] is None
|
||||||
else _parse_rgb(token_style["color"]))
|
else parse_rgb(token_style["color"]))
|
||||||
bg_color = (default_bg_color if token_style["bgcolor"] is None
|
bg_color = (default_bg_color if token_style["bgcolor"] is None
|
||||||
else _parse_rgb(token_style["bgcolor"]))
|
else parse_rgb(token_style["bgcolor"]))
|
||||||
return termstr.CharStyle(fg_color, bg_color, token_style["bold"], token_style["italic"],
|
return termstr.CharStyle(fg_color, bg_color, token_style["bold"], token_style["italic"],
|
||||||
token_style["underline"])
|
token_style["underline"])
|
||||||
default_bg_color = _parse_rgb(style.background_color)
|
|
||||||
default_style = termstr.CharStyle(bg_color=default_bg_color)
|
|
||||||
|
def syntax_highlight(text, lexer, style):
|
||||||
text = expandtabs(text)
|
text = expandtabs(text)
|
||||||
text = termstr.join("", [termstr.TermStr(
|
text = termstr.join("", [termstr.TermStr(text, char_style_for_token_type(token_type, style))
|
||||||
text, _char_style_for_token_type(token_type, default_bg_color, default_style))
|
|
||||||
for token_type, text in pygments.lex(text, lexer)])
|
for token_type, text in pygments.lex(text, lexer)])
|
||||||
text_widget = fill3.Text(text, pad_char=termstr.TermStr(" ").bg_color(default_bg_color))
|
bg_color = parse_rgb(style.background_color)
|
||||||
|
text_widget = fill3.Text(text, pad_char=termstr.TermStr(" ").bg_color(bg_color))
|
||||||
return termstr.join("\n", text_widget.text)
|
return termstr.join("\n", text_widget.text)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -75,15 +79,12 @@ def expand_str(str_):
|
||||||
|
|
||||||
class Text:
|
class Text:
|
||||||
|
|
||||||
def __init__(self, text, padding_char=" "):
|
def __init__(self, text):
|
||||||
self.padding_char = padding_char
|
|
||||||
self.lines = []
|
|
||||||
self.max_line_length = None
|
|
||||||
lines = [""] if text == "" else text.splitlines()
|
lines = [""] if text == "" else text.splitlines()
|
||||||
if text.endswith("\n"):
|
if text.endswith("\n"):
|
||||||
lines.append("")
|
lines.append("")
|
||||||
self.version = 0
|
self.version = 0
|
||||||
self[:] = lines
|
self.lines = lines
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.lines)
|
return len(self.lines)
|
||||||
|
|
@ -141,26 +142,22 @@ class Text:
|
||||||
class Code(Text):
|
class Code(Text):
|
||||||
|
|
||||||
def __init__(self, text, path, theme=NATIVE_STYLE):
|
def __init__(self, text, path, theme=NATIVE_STYLE):
|
||||||
|
try:
|
||||||
self.lexer = pygments.lexers.get_lexer_for_filename(path, text)
|
self.lexer = pygments.lexers.get_lexer_for_filename(path, text)
|
||||||
|
except pygments.util.ClassNotFound:
|
||||||
|
self.lexer = pygments.lexers.special.TextLexer()
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
padding_char = None
|
Text.__init__(self, text)
|
||||||
Text.__init__(self, text, padding_char)
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=5000)
|
@functools.lru_cache(maxsize=5000)
|
||||||
def _convert_line_themed(self, line, max_line_length, theme):
|
def _convert_line_themed(self, line, max_line_length, theme):
|
||||||
if self.padding_char is None:
|
padding_char = syntax_highlight(" ", self.lexer, self.theme)
|
||||||
self.padding_char = (" " if self.theme is None
|
highlighted_line = syntax_highlight(line, self.lexer, theme)
|
||||||
else _syntax_highlight(" ", self.lexer, self.theme))
|
return highlighted_line.ljust(max_line_length, fillchar=padding_char)
|
||||||
highlighted_line = (termstr.TermStr(line) if theme is None
|
|
||||||
else _syntax_highlight(line, self.lexer, theme))
|
|
||||||
return highlighted_line.ljust(max_line_length, fillchar=self.padding_char)
|
|
||||||
|
|
||||||
def _convert_line(self, line, max_line_length):
|
def _convert_line(self, line, max_line_length):
|
||||||
return self._convert_line_themed(line, max_line_length, self.theme)
|
return self._convert_line_themed(line, max_line_length, self.theme)
|
||||||
|
|
||||||
def syntax_highlight_all(self):
|
|
||||||
self.padding_char = None
|
|
||||||
|
|
||||||
|
|
||||||
class Decor:
|
class Decor:
|
||||||
|
|
||||||
|
|
@ -225,13 +222,13 @@ def _wrap_text_lines(words, width):
|
||||||
yield words[first_word:]
|
yield words[first_word:]
|
||||||
|
|
||||||
|
|
||||||
def wrap_text(words, width):
|
def wrap_text(words, width, pad_char=" "):
|
||||||
appearance = []
|
appearance = []
|
||||||
coords = []
|
coords = []
|
||||||
for index, line in enumerate(_wrap_text_lines(words, width)):
|
for index, line in enumerate(_wrap_text_lines(words, width)):
|
||||||
line = list(line)
|
line = list(line)
|
||||||
content = termstr.join(" ", line)
|
content = termstr.join(pad_char, line)
|
||||||
appearance.append(content.center(width))
|
appearance.append(content.center(width, pad_char))
|
||||||
cursor = index * width + round((width - len(content)) / 2)
|
cursor = index * width + round((width - len(content)) / 2)
|
||||||
for word in line:
|
for word in line:
|
||||||
coords.append((cursor, cursor + len(word)))
|
coords.append((cursor, cursor + len(word)))
|
||||||
|
|
@ -239,18 +236,15 @@ def wrap_text(words, width):
|
||||||
return appearance, coords
|
return appearance, coords
|
||||||
|
|
||||||
|
|
||||||
class Line(enum.Enum):
|
|
||||||
class_ = enum.auto()
|
|
||||||
function = enum.auto()
|
|
||||||
endpoint = enum.auto()
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(100)
|
@functools.lru_cache(100)
|
||||||
def parts_lines(source, lexer):
|
def parts_lines(source, lexer, theme):
|
||||||
cursor = 0
|
cursor = 0
|
||||||
line_num = 0
|
line_num = 0
|
||||||
line_lengths = [len(line) for line in source.splitlines(keepends=True)]
|
line_lengths = [len(line) for line in source.splitlines(keepends=True)]
|
||||||
result = [(Line.endpoint, "top", 0)]
|
white_style = termstr.CharStyle(fg_color=termstr.Color.white)
|
||||||
|
result = [(termstr.TermStr("top", white_style), 0)]
|
||||||
|
token_types = {pygments.token.Name.Class, pygments.token.Name.Function,
|
||||||
|
pygments.token.Name.Function.Magic}
|
||||||
if lexer is None:
|
if lexer is None:
|
||||||
line_num = len(source.splitlines())
|
line_num = len(source.splitlines())
|
||||||
else:
|
else:
|
||||||
|
|
@ -258,31 +252,26 @@ def parts_lines(source, lexer):
|
||||||
while position >= cursor:
|
while position >= cursor:
|
||||||
cursor += line_lengths[line_num]
|
cursor += line_lengths[line_num]
|
||||||
line_num += 1
|
line_num += 1
|
||||||
if token_type == pygments.token.Name.Class:
|
if token_type in token_types:
|
||||||
result.append((Line.class_, text, line_num - 1))
|
char_style = char_style_for_token_type(token_type, theme)
|
||||||
elif token_type in [pygments.token.Name.Function, pygments.token.Name.Function.Magic]:
|
result.append((termstr.TermStr(text, char_style), line_num - 1))
|
||||||
result.append((Line.function, text, line_num - 1))
|
result.append((termstr.TermStr("bottom", white_style), line_num - 1))
|
||||||
result.append((Line.endpoint, "bottom", line_num - 1))
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
COLOR_MAP = {Line.class_: termstr.Color.red, Line.function: termstr.Color.green,
|
|
||||||
Line.endpoint: termstr.Color.white}
|
|
||||||
|
|
||||||
|
|
||||||
class Parts:
|
class Parts:
|
||||||
|
|
||||||
def __init__(self, editor, source, lexer):
|
def __init__(self, editor, source, lexer):
|
||||||
self.editor = editor
|
self.editor = editor
|
||||||
self.lines = parts_lines(source, lexer)
|
self.source = source
|
||||||
self.parts = [termstr.TermStr(text).fg_color(COLOR_MAP[line_type])
|
self.lexer = lexer
|
||||||
for line_type, text, line_num in self.lines]
|
self.lines = parts_lines(source, lexer, editor.text_widget.theme)
|
||||||
self.width, self.height = None, None
|
self.width, self.height = None, None
|
||||||
self.is_focused = True
|
self.is_focused = True
|
||||||
self.set_cursor()
|
self.set_cursor()
|
||||||
|
|
||||||
def set_cursor(self):
|
def set_cursor(self):
|
||||||
for index, (line_type, text, line_num) in enumerate(self.lines):
|
for index, (text, line_num) in enumerate(self.lines):
|
||||||
if line_num > self.editor.cursor_y:
|
if line_num > self.editor.cursor_y:
|
||||||
self.cursor = index - 1
|
self.cursor = index - 1
|
||||||
break
|
break
|
||||||
|
|
@ -290,8 +279,8 @@ class Parts:
|
||||||
self.cursor = len(self.lines) - 1
|
self.cursor = len(self.lines) - 1
|
||||||
|
|
||||||
def _move_cursor(self, delta):
|
def _move_cursor(self, delta):
|
||||||
self.cursor = (self.cursor + delta) % len(self.parts)
|
self.cursor = (self.cursor + delta) % len(self.lines)
|
||||||
self.editor.cursor_x, self.editor.cursor_y = 0, self.lines[self.cursor][2]
|
self.editor.cursor_x, self.editor.cursor_y = 0, self.lines[self.cursor][1]
|
||||||
x, y = self.editor.view_widget.portal.position
|
x, y = self.editor.view_widget.portal.position
|
||||||
self.editor.view_widget.portal.position = x, self.editor.cursor_y - 1
|
self.editor.view_widget.portal.position = x, self.editor.cursor_y - 1
|
||||||
|
|
||||||
|
|
@ -320,11 +309,14 @@ class Parts:
|
||||||
|
|
||||||
def appearance(self):
|
def appearance(self):
|
||||||
width, height = self.dimensions
|
width, height = self.dimensions
|
||||||
parts = self.parts.copy()
|
lines = parts_lines(self.source, self.lexer, self.editor.text_widget.theme)
|
||||||
|
parts = [text for text, line_num in lines]
|
||||||
parts[self.cursor] = parts[self.cursor].invert()
|
parts[self.cursor] = parts[self.cursor].invert()
|
||||||
result, coords = wrap_text(parts, width)
|
pad_char = syntax_highlight(" ", self.editor.text_widget.lexer,
|
||||||
|
self.editor.text_widget.theme)
|
||||||
|
result, coords = wrap_text(parts, width, pad_char)
|
||||||
if len(result) > height:
|
if len(result) > height:
|
||||||
appearance, coords = wrap_text(parts, width - 1)
|
appearance, coords = wrap_text(parts, width - 1, pad_char)
|
||||||
line_num = coords[self.cursor][0] // (width - 1)
|
line_num = coords[self.cursor][0] // (width - 1)
|
||||||
if self.is_focused:
|
if self.is_focused:
|
||||||
appearance[line_num] = highlight_line(appearance[line_num])
|
appearance[line_num] = highlight_line(appearance[line_num])
|
||||||
|
|
@ -338,6 +330,9 @@ class Parts:
|
||||||
if self.is_focused:
|
if self.is_focused:
|
||||||
line_num = coords[self.cursor][0] // width
|
line_num = coords[self.cursor][0] // width
|
||||||
result[line_num] = highlight_line(result[line_num])
|
result[line_num] = highlight_line(result[line_num])
|
||||||
|
fg_color = termstr.Color.grey_100
|
||||||
|
bg_color = parse_rgb(self.editor.text_widget.theme.background_color)
|
||||||
|
result.append(termstr.TermStr("─").bg_color(bg_color).fg_color(fg_color) * width)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -345,7 +340,8 @@ class TextEditor:
|
||||||
|
|
||||||
TAB_SIZE = 4
|
TAB_SIZE = 4
|
||||||
THEMES = [pygments.styles.get_style_by_name(style)
|
THEMES = [pygments.styles.get_style_by_name(style)
|
||||||
for style in ["monokai", "fruity", "native"]] + [None]
|
for style in ["material", "monokai", "fruity", "native", "inkpot", "solarized-light",
|
||||||
|
"manni", "gruvbox-light", "perldoc", "zenburn", "friendly",]]
|
||||||
|
|
||||||
def __init__(self, text="", path="Untitled", is_left_aligned=True):
|
def __init__(self, text="", path="Untitled", is_left_aligned=True):
|
||||||
self.path = os.path.normpath(path)
|
self.path = os.path.normpath(path)
|
||||||
|
|
@ -452,10 +448,7 @@ class TextEditor:
|
||||||
return appearance
|
return appearance
|
||||||
|
|
||||||
def set_text(self, text):
|
def set_text(self, text):
|
||||||
try:
|
|
||||||
self.text_widget = Code(text, self.path)
|
self.text_widget = Code(text, self.path)
|
||||||
except pygments.util.ClassNotFound: # No lexer for path
|
|
||||||
self.text_widget = Text(text)
|
|
||||||
self.decor_widget = Decor(self.text_widget,
|
self.decor_widget = Decor(self.text_widget,
|
||||||
lambda appearance: self._add_highlights(appearance))
|
lambda appearance: self._add_highlights(appearance))
|
||||||
self.view_widget = fill3.View.from_widget(self.decor_widget)
|
self.view_widget = fill3.View.from_widget(self.decor_widget)
|
||||||
|
|
@ -727,21 +720,14 @@ class TextEditor:
|
||||||
self.set_mark()
|
self.set_mark()
|
||||||
self.jump_to_block_start()
|
self.jump_to_block_start()
|
||||||
|
|
||||||
def syntax_highlight_all(self):
|
|
||||||
self.text_widget.syntax_highlight_all()
|
|
||||||
|
|
||||||
def center_cursor(self):
|
def center_cursor(self):
|
||||||
view_x, view_y = self.view_widget.position
|
view_x, view_y = self.view_widget.position
|
||||||
new_y = max(0, self.cursor_y - self.last_height // 2)
|
new_y = max(0, self.cursor_y - self.last_height // 2)
|
||||||
self.view_widget.position = view_x, new_y
|
self.view_widget.position = view_x, new_y
|
||||||
|
|
||||||
def cycle_syntax_highlighting(self):
|
def cycle_syntax_highlighting(self):
|
||||||
self.theme_index += 1
|
self.theme_index = (self.theme_index + 1) % len(TextEditor.THEMES)
|
||||||
if self.theme_index == len(TextEditor.THEMES):
|
self.text_widget.theme = self.THEMES[self.theme_index]
|
||||||
self.theme_index = 0
|
|
||||||
theme = self.THEMES[self.theme_index]
|
|
||||||
self.text_widget.theme = theme
|
|
||||||
self.text_widget.syntax_highlight_all()
|
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
fill3.SHUTDOWN_EVENT.set()
|
fill3.SHUTDOWN_EVENT.set()
|
||||||
|
|
@ -929,10 +915,10 @@ class TextEditor:
|
||||||
terminal.ALT_b: previous_word, terminal.CTRL_LEFT: previous_word,
|
terminal.ALT_b: previous_word, terminal.CTRL_LEFT: previous_word,
|
||||||
terminal.ALT_LEFT: previous_word, terminal.ALT_BACKSPACE: delete_backward,
|
terminal.ALT_LEFT: previous_word, terminal.ALT_BACKSPACE: delete_backward,
|
||||||
terminal.ALT_CARROT: join_lines, terminal.ALT_h: highlight_block,
|
terminal.ALT_CARROT: join_lines, terminal.ALT_h: highlight_block,
|
||||||
terminal.ALT_H: highlight_block, terminal.CTRL_R: syntax_highlight_all,
|
terminal.ALT_H: highlight_block, terminal.CTRL_L: center_cursor,
|
||||||
terminal.CTRL_L: center_cursor, terminal.ALT_SEMICOLON: comment_lines,
|
terminal.ALT_SEMICOLON: comment_lines, terminal.ALT_c: cycle_syntax_highlighting,
|
||||||
terminal.ALT_c: cycle_syntax_highlighting, (terminal.CTRL_X, terminal.CTRL_C): quit,
|
(terminal.CTRL_X, terminal.CTRL_C): quit, terminal.ESC: show_parts_list,
|
||||||
terminal.ESC: show_parts_list, terminal.CTRL_K: delete_line, terminal.TAB: tab_align,
|
terminal.CTRL_K: delete_line, terminal.TAB: tab_align,
|
||||||
(terminal.CTRL_Q, terminal.TAB): insert_tab, terminal.CTRL_UNDERSCORE: undo,
|
(terminal.CTRL_Q, terminal.TAB): insert_tab, terminal.CTRL_UNDERSCORE: undo,
|
||||||
terminal.CTRL_Z: undo, terminal.CTRL_G: abort_command, terminal.INSERT: toggle_overwrite,
|
terminal.CTRL_Z: undo, terminal.CTRL_G: abort_command, terminal.INSERT: toggle_overwrite,
|
||||||
(terminal.CTRL_C, ">"): indent, (terminal.CTRL_C, "<"): dedent}
|
(terminal.CTRL_C, ">"): indent, (terminal.CTRL_C, "<"): dedent}
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,15 @@ class PartsListTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def test_parts_lines(self):
|
def test_parts_lines(self):
|
||||||
python_lexer = pygments.lexers.python.PythonLexer()
|
python_lexer = pygments.lexers.python.PythonLexer()
|
||||||
self.assertEqual(editor.parts_lines("class A:\n pass", python_lexer),
|
theme = pygments.styles.get_style_by_name("paraiso-dark")
|
||||||
[(editor.Line.endpoint, "top", 0), (editor.Line.class_, "A", 0),
|
class_charstyle = editor.char_style_for_token_type(pygments.token.Name.Class, theme)
|
||||||
(editor.Line.endpoint, "bottom", 1)])
|
self.assertEqual(editor.parts_lines("class A:\n pass", python_lexer, theme),
|
||||||
self.assertEqual(editor.parts_lines("\ndef B:", python_lexer),
|
[(termstr.TermStr("top"), 0), (termstr.TermStr("A", class_charstyle), 0),
|
||||||
[(editor.Line.endpoint, "top", 0), (editor.Line.function, "B", 1),
|
(termstr.TermStr("bottom"), 1)])
|
||||||
(editor.Line.endpoint, "bottom", 1)])
|
func_charstyle = editor.char_style_for_token_type(pygments.token.Name.Function, theme)
|
||||||
|
self.assertEqual(editor.parts_lines("\ndef B:", python_lexer, theme),
|
||||||
|
[(termstr.TermStr("top"), 0), (termstr.TermStr("B", func_charstyle), 1),
|
||||||
|
(termstr.TermStr("bottom"), 1)])
|
||||||
|
|
||||||
|
|
||||||
class ExpandTabsTestCase(unittest.TestCase):
|
class ExpandTabsTestCase(unittest.TestCase):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue