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 quote marks & brackets #519

Merged
merged 1 commit into from
Nov 15, 2024
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
9 changes: 9 additions & 0 deletions src/guiguts/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
highlight_single_quotes,
highlight_double_quotes,
remove_highlights,
highlight_quotbrac_callback,
)
from guiguts.maintext import maintext
from guiguts.mainwindow import (
Expand Down Expand Up @@ -412,6 +413,10 @@ def initialize_preferences(self) -> None:
str(DEFAULT_SCANNOS_DIR.joinpath(DEFAULT_STEALTH_SCANNOS)),
],
)
preferences.set_default(PrefKey.HIGHLIGHT_QUOTBRAC, False)
preferences.set_callback(
PrefKey.HIGHLIGHT_QUOTBRAC, highlight_quotbrac_callback
)

# Check all preferences have a default
for pref_key in PrefKey:
Expand Down Expand Up @@ -582,6 +587,10 @@ def init_search_menu(self) -> None:
"Highlight ~Double Quotes in Selection",
highlight_double_quotes,
)
menu_search.add_checkbox(
"Highlight S~urrounding Quotes & Brackets",
root().highlight_quotbrac,
)
menu_search.add_button(
"~Remove Highlights",
remove_highlights,
Expand Down
332 changes: 316 additions & 16 deletions src/guiguts/highlight.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ class HighlightTag(StrEnum):

QUOTEMARK = auto()
SPOTLIGHT = auto()
PAREN = auto()
CURLY_BRACKET = auto()
SQUARE_BRACKET = auto()
STRAIGHT_DOUBLE_QUOTE = auto()
CURLY_DOUBLE_QUOTE = auto()
STRAIGHT_SINGLE_QUOTE = auto()
CURLY_SINGLE_QUOTE = auto()


class HighlightColors:
Expand Down Expand Up @@ -39,6 +46,48 @@ class HighlightColors:
"Default": {"bg": "orange", "fg": "black"},
}

PAREN = {
"Light": {"bg": "violet", "fg": "white"},
"Dark": {"bg": "violet", "fg": "white"},
"Default": {"bg": "violet", "fg": "white"},
}

CURLY_BRACKET = {
"Light": {"bg": "blue", "fg": "white"},
"Dark": {"bg": "blue", "fg": "white"},
"Default": {"bg": "blue", "fg": "white"},
}

SQUARE_BRACKET = {
"Light": {"bg": "purple", "fg": "white"},
"Dark": {"bg": "purple", "fg": "white"},
"Default": {"bg": "purple", "fg": "white"},
}

STRAIGHT_DOUBLE_QUOTE = {
"Light": {"bg": "green", "fg": "white"},
"Dark": {"bg": "green", "fg": "white"},
"Default": {"bg": "green", "fg": "white"},
}

CURLY_DOUBLE_QUOTE = {
"Light": {"bg": "limegreen", "fg": "white"},
"Dark": {"bg": "limegreen", "fg": "white"},
"Default": {"bg": "limegreen", "fg": "white"},
}

STRAIGHT_SINGLE_QUOTE = {
"Light": {"bg": "grey", "fg": "white"},
"Dark": {"bg": "grey", "fg": "white"},
"Default": {"bg": "grey", "fg": "white"},
}

CURLY_SINGLE_QUOTE = {
"Light": {"bg": "dodgerblue", "fg": "white"},
"Dark": {"bg": "dodgerblue", "fg": "white"},
"Default": {"bg": "dodgerblue", "fg": "white"},
}


def highlight_selection(
pat: str,
Expand Down Expand Up @@ -79,25 +128,13 @@ def highlight_quotemarks(pat: str) -> None:


def highlight_single_quotes() -> None:
"""Highlight single quotes in current selection.

' APOSTROPHE
‘’ {LEFT, RIGHT} SINGLE QUOTATION MARK
‹› SINGLE {LEFT, RIGHT}-POINTING ANGLE QUOTATION MARK
‛‚ SINGLE {HIGH-REVERSED-9, LOW-9} QUOTATION MARK
"""
highlight_quotemarks("['‘’‹›‛‚]")
"""Highlight single quotes (straight or curly) in current selection."""
highlight_quotemarks("['‘’]")


def highlight_double_quotes() -> None:
"""Highlight double quotes in current selection.

" QUOTATION MARK
“” {LEFT, RIGHT} DOUBLE QUOTATION MARK
«» {LEFT, RIGHT}-POINTING DOUBLE ANGLE QUOTATION MARK
‟„ DOUBLE {HIGH-REVERSED-9, LOW-9} QUOTATION MARK
"""
highlight_quotemarks('["“”«»‟„]')
"""Highlight double quotes (straight or curly) in current selection."""
highlight_quotemarks('["“”]')


def spotlight_range(spot_range: IndexRange) -> None:
Expand Down Expand Up @@ -133,3 +170,266 @@ def _highlight_configure_tag(
background=tag_colors[theme]["bg"],
foreground=tag_colors[theme]["fg"],
)


def highlight_single_pair_bracketing_cursor(
startchar: str,
endchar: str,
tag_name: str,
tag_colors: dict[str, dict[str, str]],
*,
charpair: str = "",
) -> None:
"""
Search for a pair of matching characters that bracket the cursor and tag
them with the given tagname. If charpair is not specified, a default regex
of f"[{startchar}{endchar}]" will be used.

Args:
startchar: opening char of pair (e.g. '(')
endchar: closing chair of pair (e.g. ')')
tag_name: name of tag for highlighting (class HighlightTag)
tag_colors: dict of color information (class HighlightColors)
charpair: optional regex override for matching the pair (e.g. '[][]')
"""
maintext().tag_delete(tag_name)
_highlight_configure_tag(tag_name, tag_colors)
cursor = maintext().get_insert_index().index()

(top_index, bot_index) = get_screen_window_coordinates()

# search backward for the startchar
startindex = search_for_base_character_in_pair(
top_index,
cursor,
bot_index,
startchar,
endchar,
charpair=charpair,
backwards=True,
)
if not startindex:
return

# search forward for the endchar
endindex = search_for_base_character_in_pair(
top_index, cursor, bot_index, startchar, endchar, charpair=charpair
)

if not (startindex and endindex):
return

maintext().tag_add(tag_name, startindex, maintext().index(f"{startindex}+1c"))
maintext().tag_add(tag_name, endindex, maintext().index(f"{endindex}+1c"))


def get_screen_window_coordinates() -> tuple[str, str]:
"""
Find start and end coordinates for viewport (with a margin of
offscreen text added for padding).
"""
# A magic number cribbed from Tk.pm's TextEdit.pm.
# This is how many rows to explore beyond the visible viewport.
offscreen_rows = 80

(top_frac, bot_frac) = maintext().focus_widget().yview()
end_index = maintext().rowcol("end")

# Don't try to go beyond the boundaries of the document.
#
# {top,bot}_frac contain a fractional number representing a percentage into
# the document; do some math to calculate what the top or bottom row in the
# viewport should be, then use min/max to make sure that value isn't less
# than 0 or more than the total row count.
top_line = max(int((top_frac * end_index.row) - offscreen_rows), 0)
bot_line = min(int((bot_frac * end_index.row) + offscreen_rows), end_index.row)

return (f"{top_line}.0", f"{bot_line}.0")


def search_for_base_character_in_pair(
top_index: str,
searchfromindex: str,
bot_index: str,
startchar: str,
endchar: str,
*,
backwards: bool = False,
charpair: str = "",
) -> str:
"""
If searching backward, count characters (e.g. parenthesis) until finding a
startchar which does not have a forward matching endchar.

(<= search backward will return this index
()
START X HERE
( ( ) () )
)<== search forward will return this index

If searching forward, count characters until finding an endchar which does
not have a rearward matching startchar.

Default search direction is forward.

If charpair is not specified, a default regex is constructed from startchar,
endchar using f"[{startchar}{endchar}]". For example,

startchar='(', endchar=')' results in: charpar='[()]'
"""

forwards = True
if backwards:
forwards = False

if not charpair:
if startchar == endchar:
charpair = startchar
else:
charpair = f"[{startchar}{endchar}]"

if forwards:
plus_one_char = endchar
search_end_index = bot_index
index_offset = " +1c"
done_index = maintext().index("end")
else:
plus_one_char = startchar
search_end_index = top_index
index_offset = ""
done_index = "1.0"

at_done_index = False
count = 0

while True:
searchfromindex = maintext().search(
charpair,
searchfromindex,
search_end_index,
backwards=backwards,
forwards=forwards,
regexp=True,
)

if not searchfromindex:
break

# get one character at the identified index
char = maintext().get(searchfromindex)
if char == plus_one_char:
count += 1
else:
count -= 1

if count == 1:
break

# boundary condition exists when first char in widget is the match char
# need to be able to determine if search tried to go past index '1.0'
# if so, set index to undef and return.
if at_done_index:
searchfromindex = ""
break

if searchfromindex == done_index:
at_done_index = True

searchfromindex = maintext().index(f"{searchfromindex}{index_offset}")

return searchfromindex


def highlight_parens_around_cursor() -> None:
"""Highlight pair of parens that most closely brackets the cursor."""
highlight_single_pair_bracketing_cursor(
"(",
")",
HighlightTag.PAREN,
HighlightColors.PAREN,
)


def highlight_curly_brackets_around_cursor() -> None:
"""Highlight pair of curly brackets that most closely brackets the cursor."""
highlight_single_pair_bracketing_cursor(
"{",
"}",
HighlightTag.CURLY_BRACKET,
HighlightColors.CURLY_BRACKET,
)


def highlight_square_brackets_around_cursor() -> None:
"""Highlight pair of square brackets that most closely brackets the cursor."""
highlight_single_pair_bracketing_cursor(
"[",
"]",
HighlightTag.SQUARE_BRACKET,
HighlightColors.SQUARE_BRACKET,
charpair="[][]",
)


def highlight_double_quotes_around_cursor() -> None:
"""Highlight pair of double quotes that most closely brackets the cursor."""
highlight_single_pair_bracketing_cursor(
'"',
'"',
HighlightTag.STRAIGHT_DOUBLE_QUOTE,
HighlightColors.STRAIGHT_DOUBLE_QUOTE,
)
highlight_single_pair_bracketing_cursor(
"“",
"”",
HighlightTag.CURLY_DOUBLE_QUOTE,
HighlightColors.CURLY_DOUBLE_QUOTE,
)


def highlight_single_quotes_around_cursor() -> None:
"""Highlight pair of single quotes that most closely brackets the cursor."""
highlight_single_pair_bracketing_cursor(
"'",
"'",
HighlightTag.STRAIGHT_SINGLE_QUOTE,
HighlightColors.STRAIGHT_SINGLE_QUOTE,
)
highlight_single_pair_bracketing_cursor(
"‘",
"’",
HighlightTag.CURLY_SINGLE_QUOTE,
HighlightColors.CURLY_SINGLE_QUOTE,
)


def highlight_quotbrac_callback(value: bool) -> None:
"""Callback when highlight_quotbrac preference is changed."""
if value:
highlight_quotbrac()
else:
remove_highlights_quotbrac()


def highlight_quotbrac() -> None:
"""Highlight all the character pairs that most closely bracket the cursor."""
if preferences.get(PrefKey.HIGHLIGHT_QUOTBRAC):
highlight_parens_around_cursor()
highlight_curly_brackets_around_cursor()
highlight_square_brackets_around_cursor()
highlight_double_quotes_around_cursor()
highlight_single_quotes_around_cursor()


def remove_highlights_quotbrac() -> None:
"""Remove highlights for quotes & brackets"""
for tag in (
HighlightTag.PAREN,
HighlightTag.CURLY_BRACKET,
HighlightTag.SQUARE_BRACKET,
HighlightTag.STRAIGHT_DOUBLE_QUOTE,
HighlightTag.CURLY_DOUBLE_QUOTE,
HighlightTag.STRAIGHT_SINGLE_QUOTE,
HighlightTag.CURLY_SINGLE_QUOTE,
):
maintext().tag_delete(tag)
Loading
Loading