Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight only visible text #35

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,7 @@ dmypy.json

# Mac stuff
.DS_Store
chlorophyll/.DS_Store
chlorophyll/.DS_Store

# Extras for development
test.py
2 changes: 1 addition & 1 deletion chlorophyll/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .codeview import CodeView
from .codeview import CodeView # noqa: F401
108 changes: 46 additions & 62 deletions chlorophyll/codeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from tkinter.font import Font
from typing import Any

from pygments import lex
import pygments.lexers

from pyperclip import copy
from tklinenums import TkLineNumbers
from toml import load
Expand All @@ -29,30 +29,37 @@ def __init__(
tab_width: int = 4,
**kwargs,
) -> None:
# Set up the frame
self._frame = ttk.Frame(master)
self._frame.grid_rowconfigure(0, weight=1)
self._frame.grid_columnconfigure(1, weight=1)

# Set kwargs
kwargs.setdefault("wrap", "none")
kwargs.setdefault("font", ("monospace", 11))

# Finish setting up the text widget
super().__init__(self._frame, **kwargs)
super().grid(row=0, column=1, sticky="nswe")

# Set up the line numbers and scrollbars
self._line_numbers = TkLineNumbers(self._frame, self, justify=kwargs.get("justify", "left"))
self._vs = ttk.Scrollbar(self._frame, orient="vertical", command=self.yview)
self._hs = ttk.Scrollbar(self._frame, orient="horizontal", command=self.xview)

# Grid the line numbers and scrollbars
self._line_numbers.grid(row=0, column=0, sticky="ns")
self._vs.grid(row=0, column=2, sticky="ns")
self._hs.grid(row=1, column=1, sticky="we")

# Configure the text widget
super().configure(
yscrollcommand=self.vertical_scroll,
xscrollcommand=self.horizontal_scroll,
tabs=Font(font=kwargs["font"]).measure(" " * tab_width),
)

# Set up the key bindings
contmand = "Command" if self._windowingsystem == "aqua" else "Control"

super().bind(f"<{contmand}-c>", self._copy, add=True)
Expand All @@ -61,10 +68,16 @@ def __init__(
super().bind(f"<{contmand}-Shift-Z>", self.redo, add=True)
super().bind("<<ContentChanged>>", self.scroll_line_update, add=True)

# Set up the proxy
self._orig = f"{self._w}_widget"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._cmd_proxy)

# Create any necessary variables
self._current_text: str = self.get("1.0", "end-1c")
self._current_visible_area: tuple[str, str] = self.index("@0,0"), self.index(f"@0,{self.winfo_height()}")

# Set up the lexer and color scheme along with the MLCDS tag
self._set_lexer(lexer)
self._set_color_scheme(color_scheme)

Expand All @@ -79,7 +92,7 @@ def redo(self, event: Event | None = None) -> None:
except TclError:
pass

def _paste(self, *_):
def _paste(self, *_) -> str:
insert = self.index(f"@0,0 + {self.cget('height') // 2} lines")

with suppress(TclError):
Expand All @@ -91,7 +104,7 @@ def _paste(self, *_):

return "break"

def _copy(self, *_):
def _copy(self, *_) -> str:
text = self.get("sel.first", "sel.last")
if not text:
text = self.get("insert linestart", "insert lineend")
Expand All @@ -102,32 +115,15 @@ def _copy(self, *_):

def _cmd_proxy(self, command: str, *args) -> Any:
try:
if command in {"insert", "delete", "replace"}:
start_line = int(str(self.tk.call(self._orig, "index", args[0])).split(".")[0])
end_line = start_line
if len(args) == 3:
end_line = int(str(self.tk.call(self._orig, "index", args[1])).split(".")[0]) - 1
result = self.tk.call(self._orig, command, *args)
except TclError as e:
error = str(e)
if 'tagged with "sel"' in error or "nothing to" in error:
return ""
raise e from None

if command == "insert":
if not args[0] == "insert":
start_line -= 1
lines = args[1].count("\n")
if lines == 1:
self.highlight_line(f"{start_line}.0")
else:
self.highlight_area(start_line, start_line + lines)
self.event_generate("<<ContentChanged>>")
elif command in {"replace", "delete"}:
if start_line == end_line:
self.highlight_line(f"{start_line}.0")
else:
self.highlight_area(start_line, end_line)
if command in ("insert", "replace", "delete"):
self.highlight()
self.event_generate("<<ContentChanged>>")

return result
Expand All @@ -137,52 +133,38 @@ def _setup_tags(self, tags: dict[str, str]) -> None:
if isinstance(value, str):
self.tag_configure(f"Token.{key}", foreground=value)

def highlight_line(self, index: str) -> None:
line_num = int(self.index(index).split(".")[0])
for tag in self.tag_names(index=None):
if tag.startswith("Token"):
self.tag_remove(tag, f"{line_num}.0", f"{line_num}.end")
def highlight(self) -> None:
"""Highlights all the visible text in the text widget"""

line_text = self.get(f"{line_num}.0", f"{line_num}.end")
start_col = 0
# Get visible area and text
visible_area: tuple[str] = self.index("@0,0"), self.index(f"@0,{self.winfo_height()}")
visible_text: str = self.get(*visible_area)

for token, text in lex(line_text, self._lexer()):
token = str(token)
end_col = start_col + len(text)
if token not in {"Token.Text.Whitespace", "Token.Text"}:
self.tag_add(token, f"{line_num}.{start_col}", f"{line_num}.{end_col}")
start_col = end_col
# Check if anything has changed
if visible_area == self._current_visible_area and visible_text == self._current_text:
return

self._current_text = visible_text

# Get line offset
line_offset: int = int(self.index("@0,0").split(".")[0]) - 1
# Not used yet (remove F841 when used)

def highlight_all(self) -> None:
for tag in self.tag_names(index=None):
if tag.startswith("Token"):
# Remove all tags
for tag in self.tag_names():
if tag.startswith("Token."):
self.tag_remove(tag, "1.0", "end")

lines = self.get("1.0", "end")
line_offset = lines.count("\n") - lines.lstrip().count("\n")
# Highlight the text
start_index = str(self.tk.call(self._orig, "index", f"1.0 + {line_offset} lines"))

for token, text in lex(lines, self._lexer()):
for token, text in pygments.lex(visible_text, self._lexer):
token = str(token)
end_index = self.index(f"{start_index} + {len(text)} chars")
if token not in {"Token.Text.Whitespace", "Token.Text"}:
self.tag_add(token, start_index, end_index)
start_index = end_index

def highlight_area(self, start_line: int | None = None, end_line: int | None = None) -> None:
for tag in self.tag_names(index=None):
if tag.startswith("Token"):
self.tag_remove(tag, f"{start_line}.0", f"{end_line}.end")

text = self.get(f"{start_line}.0", f"{end_line}.end")
line_offset = text.count("\n") - text.lstrip().count("\n")
start_index = str(self.tk.call(self._orig, "index", f"{start_line}.0 + {line_offset} lines"))
for token, text in lex(text, self._lexer()):
token = str(token)
end_index = self.index(f"{start_index} + {len(text)} indices")
if token not in {"Token.Text.Whitespace", "Token.Text"}:
self.tag_add(token, start_index, end_index)
start_index = end_index
self._current_visible_area = visible_area

def _set_color_scheme(self, color_scheme: dict[str, dict[str, str | int]] | str | None) -> None:
if isinstance(color_scheme, str) and color_scheme in self._builtin_color_schemes:
Expand All @@ -196,12 +178,12 @@ def _set_color_scheme(self, color_scheme: dict[str, dict[str, str | int]] | str
self.configure(**config)
self._setup_tags(tags)

self.highlight_all()
self.highlight()

def _set_lexer(self, lexer: pygments.lexers.Lexer) -> None:
self._lexer = lexer

self.highlight_all()
self.highlight()

def __setitem__(self, key: str, value) -> None:
self.configure(**{key: value})
Expand Down Expand Up @@ -246,13 +228,15 @@ def destroy(self) -> None:
BaseWidget.destroy(widget)
BaseWidget.destroy(self._frame)

def horizontal_scroll(self, first: str | float, last: str | float) -> CodeView:
def horizontal_scroll(self, first: str | float, last: str | float) -> None:
self._hs.set(first, last)

def vertical_scroll(self, first: str | float, last: str | float) -> CodeView:
def vertical_scroll(self, first: str | float, last: str | float) -> None:
self._current_visible_area = self.index("@0,0"), self.index(f"@0,{self.winfo_height()}")
self.highlight()
self._vs.set(first, last)
self._line_numbers.redraw()

def scroll_line_update(self, event: Event | None = None) -> CodeView:
def scroll_line_update(self, _: Event | None = None) -> None:
self.horizontal_scroll(*self.xview())
self.vertical_scroll(*self.yview())
self.vertical_scroll(*self.yview())
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ line-length = 110
line_length = 100
profile = "black"
multi_line_output = 3

[tool.ruff]
line-length = 110