From f7b5c28fab10b606fbf984b6b97baebbda39dd16 Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 6 Sep 2025 20:25:28 -0400 Subject: [PATCH 1/2] Replace unnecessary regex use with str.replace. --- core/snippets/snippets_insert_raw_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/snippets/snippets_insert_raw_text.py b/core/snippets/snippets_insert_raw_text.py index 721f5cc821..8e7174c0b9 100644 --- a/core/snippets/snippets_insert_raw_text.py +++ b/core/snippets/snippets_insert_raw_text.py @@ -150,7 +150,7 @@ def format_tabs(text: str) -> str: spaces_per_tab: int = settings.get("user.snippet_raw_text_spaces_per_tab") if spaces_per_tab < 0: return text - return re.sub(r"\t", " " * spaces_per_tab, text) + return text.replace("\t", " " * spaces_per_tab) def parse_snippet(body: str): From e5a8db170d3ca05b01707b414089422497aadc37 Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 6 Sep 2025 20:45:53 -0400 Subject: [PATCH 2/2] Escape/unescape $ in snippet substitutions (fixes #2015). Also handle $TM_SELECTED_TEXT/$CLIPBOARD more flexibly. --- core/snippets/snippets_insert.py | 1 + core/snippets/snippets_insert_raw_text.py | 55 +++++++++++++---------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/core/snippets/snippets_insert.py b/core/snippets/snippets_insert.py index 343b45327f..8d23726891 100644 --- a/core/snippets/snippets_insert.py +++ b/core/snippets/snippets_insert.py @@ -72,6 +72,7 @@ def compute_snippet_body_with_substitutions( body = snippet.body if substitutions: for k, v in substitutions.items(): + v = v.replace("$", r"\$") reg = re.compile(rf"\${k}|\$\{{{k}\}}") if not reg.search(body): raise ValueError( diff --git a/core/snippets/snippets_insert_raw_text.py b/core/snippets/snippets_insert_raw_text.py index 8e7174c0b9..f6069d601a 100644 --- a/core/snippets/snippets_insert_raw_text.py +++ b/core/snippets/snippets_insert_raw_text.py @@ -21,7 +21,7 @@ desc="""If true, inserting snippets as raw text will always be done through pasting""", ) -RE_STOP = re.compile(r"\$(\d+|\w+)|\$\{(\d+|\w+)\}|\$\{(\d+|\w+):(.+)\}") +RE_STOP = re.compile(r"\\?(?:\$(\d+|\w+)|\$\{(\d+|\w+)\}|\$\{(\d+|\w+):(.+)\})") LAST_SNIPPET_HOLE_KEY_VALUE = 1000 @@ -157,34 +157,41 @@ def parse_snippet(body: str): # Some IM services will send the message on a tab body = format_tabs(body) - # Replace variable with appropriate value/text - body = re.sub(r"\$TM_SELECTED_TEXT", lambda _: actions.edit.selected_text(), body) - body = re.sub(r"\$CLIPBOARD", lambda _: actions.clip.text(), body) - lines = body.splitlines() stops: list[Stop] = [] for i, line in enumerate(lines): - match = RE_STOP.search(line) - - while match: - stops.append( - Stop( - name=match.group(1) or match.group(2) or match.group(3), - rows_up=len(lines) - i - 1, - columns_left=0, - row=i, - col=match.start(), - ) - ) - - # Remove tab stops and variables. - stop_text = match.group(0) - default_value = match.group(4) or "" - line = line.replace(stop_text, default_value, 1) - # Might have multiple stops on the same line - match = RE_STOP.search(line) + start = 0 + while match := RE_STOP.search(line, start): + stop_text = match.group(0) + if stop_text[0] == "\\": + # Remove escape for $ + value = stop_text[1:] + # Don't match now-unescaped $ as a stop + start = match.end() - 1 + else: + name = match.group(1) or match.group(2) or match.group(3) + value = match.group(4) or "" + + match name: + case "TM_SELECTED_TEXT": + value = actions.edit.selected_text() or value + case "CLIPBOARD": + value = actions.clip.text() or value + case _: + stops.append( + Stop( + name=name, + rows_up=len(lines) - i - 1, + columns_left=0, + row=i, + col=match.start(), + ) + ) + + # Remove/replace escaped $, tab stops and variables. + line = line.replace(stop_text, value, 1) # Update existing line lines[i] = line