Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/biscuit/editor/text/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ def on_scroll(self, *_) -> None:
self.linenumbers.redraw()
if not self.minimalist:
self.minimap.redraw()
self.text.update_indent_guides()
self.event_generate("<<Scroll>>")

def unsupported_file(self) -> None:
Expand Down
234 changes: 192 additions & 42 deletions src/biscuit/editor/text/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import threading
import tkinter as tk
import tkinter.font as tkfont
import typing
from collections import deque
from tkinter.messagebox import askokcancel
Expand Down Expand Up @@ -77,6 +78,11 @@ def __init__(
self.lsp: bool = False
self.current_indent_level = 0
self.insert_final_newline = False
self.indent_guides: list[tk.Frame] = []
self.indent_guide_pool: list[tk.Frame] = []
self.active_indent_level = -1
self.active_start_line = -1
self.active_end_line = -1

self.hover_after = None
self.last_hovered = None
Expand Down Expand Up @@ -129,7 +135,6 @@ def __init__(
self._edit_stack_index = -1

def config_tags(self):
self.indentguide_stipple = self.base.resources.indent_guide

self.tag_config(tk.SEL, background=self.base.theme.editors.selection)
self.tag_config(
Expand All @@ -154,19 +159,19 @@ def config_tags(self):

self.tag_configure(
"indent_guide",
bgstipple=f"@{self.indentguide_stipple}",
background=self.base.theme.border,
background=self.base.theme.editors.indent_guide,
)
self.tag_configure(
"current_indent_guide",
bgstipple=f"@{self.indentguide_stipple}",
background=self.base.theme.secondary_foreground,
background=self.base.theme.editors.indent_guide_active,
)

self.tag_raise(tk.SEL, "hover")
self.tag_raise(tk.SEL, "currentline")
self.tag_raise(tk.SEL, "currentword")
self.tag_raise("currentword", "currentline")
self.tag_raise("currentline", "indent_guide")
self.tag_raise("currentline", "current_indent_guide")

self.tag_config(
"activebracket", background=self.base.theme.editors.activebracket
Expand Down Expand Up @@ -305,47 +310,173 @@ def diagnostic_hover(self, severity: int) -> str:

def update_indent_guides(self) -> None:
if self.minimalist or not self.base.config.render_indent_guides:
for guide in self.indent_guides:
guide.place_forget()
self.indent_guide_pool.extend(self.indent_guides)
self.indent_guides = []
return

self.tag_remove("indent_guide", "1.0", "end")
self.tag_remove("current_indent_guide", "1.0", "end")
lines = self.get("1.0", "end-1c").split("\n")

self.current_indent_level = self.get_current_indent_level() - 1
try:
first_line = int(self.index("@0,0").split(".")[0])
last_line = int(self.index(f"@0,{self.winfo_height()}").split(".")[0])
except Exception:
return

# Buffer for smooth scrolling
first_line = max(1, first_line - 5)
last_line = min(int(self.index(tk.END).split(".")[0]), last_line + 5)

# Clear existing guides
for guide in self.indent_guides:
guide.place_forget()
self.indent_guide_pool.extend(self.indent_guides)
self.indent_guides = []

self.get_active_block_info()

# Cache indentation levels to handle empty lines better
indents = {}
for line_number in range(first_line, last_line + 1):
line = self.get(f"{line_number}.0", f"{line_number}.end")
if line.strip():
indents[line_number] = self.calculate_indent_level(line)
else:
indents[line_number] = -1 # Mark as empty

self.char_width = tkfont.Font(font=self["font"]).measure(" ")
for line_number in range(first_line, last_line + 1):
indent_level = indents[line_number]
if indent_level == -1:
prev_indent = 0
for l in range(line_number - 1, max(1, line_number - 50), -1):
if l in indents and indents[l] != -1:
prev_indent = indents[l]
break

next_indent = 0
for l in range(line_number + 1, min(last_line + 60, line_number + 50)):
# Check beyond last_line if needed
cached = indents.get(l)
if cached is not None and cached != -1:
next_indent = cached
break
elif cached is None:
# Fetch and cache
line = self.get(f"{l}.0", f"{l}.end")
if line.strip():
next_indent = self.calculate_indent_level(line)
indents[l] = next_indent
break
else:
indents[l] = -1

indent_level = min(prev_indent, next_indent) if next_indent > 0 else prev_indent

for line_number, line in enumerate(lines, start=1):
indent_level = self.calculate_indent_level(line)
if indent_level > 0:
self.add_indent_guide(line_number, indent_level)
self.add_indent_guides_to_line(line_number, indent_level)

def calculate_indent_level(self, line: str) -> int:
indent = len(line) - len(line.lstrip())
expanded = line.expandtabs(self.tab_spaces)
indent = len(expanded) - len(expanded.lstrip())
return indent // self.tab_spaces

def add_indent_guide(self, line_number: int, indent_level: int) -> None:
def get_char_index_at_col(self, line: str, col: int) -> int:
current_col = 0
for i, char in enumerate(line):
if current_col >= col:
return i
if char == "\t":
current_col += self.tab_spaces - (current_col % self.tab_spaces)
else:
current_col += 1
return len(line)

def add_indent_guides_to_line(self, line_number: int, indent_level: int) -> None:
dline = self.dlineinfo(f"{line_number}.0")
if not dline:
return

x_base, y, _, h, _ = dline
for level in range(indent_level):
start_index = f"{line_number}.{level * self.tab_spaces}"
end_index = f"{line_number}.{level * self.tab_spaces + 1}"
self.tag_add(
(
"current_indent_guide"
if level == self.current_indent_level
else "indent_guide"
),
start_index,
end_index,
col = level * self.tab_spaces
x = x_base + (col * self.char_width)

# Use a frame from the pool or create a new one
if self.indent_guide_pool:
guide = self.indent_guide_pool.pop()
else:
guide = tk.Frame(self, width=1, highlightthickness=0, bd=0)

guide.config(
bg=(
self.base.theme.editors.indent_guide_active
if (
level == self.active_indent_level
and self.active_start_line <= line_number <= self.active_end_line
)
else self.base.theme.editors.indent_guide
)
)
# Find char index for click-to-goto
line = self.get(f"{line_number}.0", f"{line_number}.end")
char_idx = self.get_char_index_at_col(line, col)
idx = f"{line_number}.{char_idx}"

def get_current_indent_level(self) -> int:
prev = self.get("insert-1l linestart", "insert-1l lineend")
line = self.get("insert linestart", "insert lineend")
next_line = self.get("insert+1l linestart", "insert+1l lineend")
guide.bind("<Button-1>", lambda _, idx=idx: self.goto(idx))
guide.place(x=x, y=y, width=1, height=h)
self.indent_guides.append(guide)

return max(
self.calculate_indent_level(prev),
self.calculate_indent_level(line),
self.calculate_indent_level(next_line),
)
def get_active_block_info(self) -> None:
self.active_indent_level = -1
self.active_start_line = -1
self.active_end_line = -1

if self.highlighter and self.highlighter.ts and self.highlighter.ts.tree:
try:
line, col = self._tk_index_to_point(tk.INSERT)
node = self.highlighter.ts.tree.root_node.descendant_for_point_range(
(line, col), (line, col)
)

# Block-like node types across various languages
BLOCK_TYPES = (
"block", "compound_statement", "function_definition", "method_definition",
"class_definition", "if_statement", "for_statement", "while_statement",
"do_statement", "try_statement", "with_statement", "switch_statement",
"match_expression", "match_arm", "unsafe_block", "impl_item", "trait_item",
"enum_definition", "struct_definition", "union_definition", "macro_definition",
"macro_invocation", "lambda", "argument_list", "parameters", "binary_expression",
"parenthesized_expression", "array_expression", "dictionary_expression",
"list_expression", "set_expression", "tuple_expression", "object_expression",
"module", "translation_unit"
)

# Find the innermost block-like node that spans multiple lines
temp = node
while temp:
if temp.type in BLOCK_TYPES:
# Only consider it an active block if it spans multiple lines
# or if it's the only block we have.
if temp.start_point[0] != temp.end_point[0] or temp.parent is None:
# For standard 'block' nodes (the actual { } content),
# we usually want to align with the parent keyword's indentation.
if temp.type == "block" and temp.parent:
self.active_indent_level = temp.parent.start_point[1] // self.tab_spaces
else:
self.active_indent_level = temp.start_point[1] // self.tab_spaces

self.active_start_line = temp.start_point[0] + 1
self.active_end_line = temp.end_point[0] + 1
return
temp = temp.parent
except Exception:
pass

# Fallback to current line's indentation level
line_content = self.get("insert linestart", "insert lineend")
self.active_indent_level = self.calculate_indent_level(line_content) - 1
self.active_start_line = 1
self.active_end_line = int(self.index(tk.END).split(".")[0])

# TODO this wont work properly
# write a custom ast, parser for bracket matching
Expand Down Expand Up @@ -1360,13 +1491,20 @@ def clear_all_selection(self):
self.tag_remove(tk.SEL, 1.0, tk.END)

def highlight_current_line(self, *_):
self.tag_remove("currentline", 1.0, tk.END)
if self.minimalist or self.tag_ranges(tk.SEL):
self.tag_remove("currentline", 1.0, tk.END)
return

line = int(self.index(tk.INSERT).split(".")[0])
start = str(float(line))
end = str(float(line + 1))
start = f"{line}.0"
end = f"{line + 1}.0"

# Only update if the line actually changed
current_ranges = self.tag_ranges("currentline")
if current_ranges and self.index(current_ranges[0]) == self.index(start):
return

self.tag_remove("currentline", 1.0, tk.END)
self.tag_add("currentline", start, end)

def select_line(self, line):
Expand All @@ -1380,14 +1518,26 @@ def select_line(self, line):
self.move_cursor(end)

def highlight_current_word(self):
self.tag_remove("currentword", 1.0, tk.END)
if self.tag_ranges(tk.SEL):
self.tag_remove("currentword", 1.0, tk.END)
return

if word := re.findall(r"\w+", self.get("insert wordstart", "insert wordend")):
# TODO: do not highlight keywords, parts of strings, etc.
self.highlight_pattern(f"\\y{word[0]}\\y", "currentword", regexp=True)
# self.highlight_pattern(f"\\y{word[0]}\\y", "currentword", start="insert wordend", regexp=True)
word_range = self.tag_ranges(tk.INSERT + " wordstart") # dummy to get word
# This is a bit tricky, let's just use the current simple logic but only if insertion changed

new_word = self.get("insert wordstart", "insert wordend").strip()
if not new_word or not re.match(r"^\w+$", new_word):
self.tag_remove("currentword", 1.0, tk.END)
return

# Optimization: only re-highlight if the word is different
# (This is still a bit expensive as it searches the whole file)
# But we can limit the search to visible range!
first_line = int(self.index("@0,0").split(".")[0])
last_line = int(self.index(f"@0,{self.winfo_height()}").split(".")[0])

self.tag_remove("currentword", 1.0, tk.END)
self.highlight_pattern(f"\\y{new_word}\\y", "currentword", start=f"{first_line}.0", end=f"{last_line + 1}.0", regexp=True)

def highlight_pattern(self, pattern, tag, start="1.0", end=tk.END, regexp=False):
start = self.index(start)
Expand Down
2 changes: 2 additions & 0 deletions src/biscuit/settings/theme/gruvbox_dark.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,6 @@ def __init__(self, *args, **kwds) -> None:

self.editors.bracket_colors = ("ffd700", "da70d6", "179fff")
self.editors.activebracket = "#d79921"
self.editors.indent_guide = "#3c3836"
self.editors.indent_guide_active = "#7c6f64"
self.editors.hyperlink = "#4583b6"
2 changes: 2 additions & 0 deletions src/biscuit/settings/theme/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ def __init__(self, *args, **kwargs) -> None:
self.found = "#dbe6c2"
self.foundcurrent = "green"
self.hovertag = "#d5d5d5"
self.indent_guide = "#d5d5d5"
self.indent_guide_active = "#919191"

self.text = ThemeObject(self)
self.minimap = FrameThemeObject(self)
Expand Down
3 changes: 3 additions & 0 deletions src/biscuit/settings/theme/vscdark.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def __init__(self, *args, **kwds) -> None:
self.editors.linenumbers.breakpoint.background = "#6e1b13"
self.editors.linenumbers.breakpoint.highlightbackground = "#e51400"

self.editors.indent_guide = "#333333"
self.editors.indent_guide_active = "#5a5a5a"

self.layout.statusbar.button_highlighted.update(
bg=self.biscuit,
fg="white",
Expand Down
Loading