From 6d9566e2a284bbd4f56412cd837114828a687a51 Mon Sep 17 00:00:00 2001 From: dam024 Date: Wed, 11 Mar 2026 23:02:02 +0100 Subject: [PATCH 1/4] Add autoclosing of an environment when using the latex auto fill If the env_auto_trigger settings is true, LaTeXTools asks the user to enter an environment name via a WindowCommand. This modification ensures that the environment is closed automatically when the user triggers this feature with \begin{...} --- latextools/latex_env_completions.py | 34 +++++++++++++++++++++++++++++ latextools/latex_fill_all.py | 14 ++++++++++++ 2 files changed, 48 insertions(+) diff --git a/latextools/latex_env_completions.py b/latextools/latex_env_completions.py index f1f7c35e..1190b4ce 100644 --- a/latextools/latex_env_completions.py +++ b/latextools/latex_env_completions.py @@ -63,6 +63,40 @@ def get_completions(self, view, prefix, line): return (display, values) + def on_selection(self, view, insert_text): + # The \end{...} is added only if there is a single cursor and if the 4 characters before the cursor are "\end" + sel = view.sel() + #for i in range(len(sel)): + if len(sel) == 1: + i = 0 + cursor = sel[i].end() + + # Determines the characters before the cursor + before_bracket = cursor - 1 - len(insert_text) + begin = view.substr(sublime.Region(before_bracket-4, before_bracket)) + + # Returns if we should not insert the closing environment + if begin == "\\end": + return + + # First, move the cursor after the {} + new_region = sublime.Region(cursor + 1) + + sel.clear() + sel.add(new_region) + #sel[i] = new_region + + # Insert the \end{...} + view.run_command("insert", {"characters": f"\n\n\\end{{{insert_text}}}"}) + + # Place the cursor between the \begin{...} and the \end{...} + new_region = sublime.Region(cursor + 2) + sel.clear() + sel.add(new_region) + + # Add a \t for correct indentation + view.run_command("insert", {"characters": "\t"}) + def matches_line(self, line): return bool(BEGIN_END_BEFORE_REGEX.match(line)) diff --git a/latextools/latex_fill_all.py b/latextools/latex_fill_all.py index 3e73f770..2237519f 100644 --- a/latextools/latex_fill_all.py +++ b/latextools/latex_fill_all.py @@ -80,6 +80,19 @@ def get_completions(self, view, prefix, line): """ return None + def on_selection(self, view, insert_text): + """ + Code executed after the user has selected a completion and after the completion has been inserted in the view + + :param view: + The current `View` being edited + + :param insert_text: + Text used for the completion + + """ + pass + def matches_line(self, line): """ Checks if this plugin matches the current line @@ -1108,6 +1121,7 @@ def on_done(i, text=""): "remove_regions": self.regions_to_tuples(remove_regions), }, ) + completion_type.on_selection(view, insert_text) # track visible input quick panels to provide key binding context VISIBLE_OVERLAYS.add(window.id()) From 2dc60771a5c44c717347fbf091ddd3511c58372e Mon Sep 17 00:00:00 2001 From: dam024 Date: Thu, 12 Mar 2026 19:41:57 +0100 Subject: [PATCH 2/4] Adding support to multiple cursors and bug fixes The following bugs have been fixed: - When called from the latextools_fill_all TextCommand, the autoclose is not performed - When completing another command than \begin{...}, the cursor is moved by one character, being after the closing bracket. --- latextools/latex_env_completions.py | 37 ++++++++++++++--------------- latextools/latex_fill_all.py | 13 +++++++--- latextools/utils/sublime_utils.py | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/latextools/latex_env_completions.py b/latextools/latex_env_completions.py index 1190b4ce..aebb741d 100644 --- a/latextools/latex_env_completions.py +++ b/latextools/latex_env_completions.py @@ -8,6 +8,7 @@ from .utils import analysis from .utils.settings import get_setting from .utils.tex_directives import get_tex_root +from .utils.sublime_utils import move_cursor_relative BEGIN_END_BEFORE_REGEX = re.compile(r"([^{}\[\]]*)\{(?:\][^{}\[\]]*\[)?(?:nigeb|dne)\\") """ @@ -63,12 +64,15 @@ def get_completions(self, view, prefix, line): return (display, values) - def on_selection(self, view, insert_text): + def on_selection(self, view, insert_text, should_complete): + # Do nothing the fill helper was called to replace the content of a command + if not should_complete: + return + # The \end{...} is added only if there is a single cursor and if the 4 characters before the cursor are "\end" sel = view.sel() - #for i in range(len(sel)): - if len(sel) == 1: - i = 0 + take_selection = [True]*len(sel) # Indicates if we need the Region at index i will undergo autoclose of environment + for i in range(len(sel)): cursor = sel[i].end() # Determines the characters before the cursor @@ -77,25 +81,20 @@ def on_selection(self, view, insert_text): # Returns if we should not insert the closing environment if begin == "\\end": - return - - # First, move the cursor after the {} - new_region = sublime.Region(cursor + 1) + take_selection[i] = False - sel.clear() - sel.add(new_region) - #sel[i] = new_region + # First, move the cursor after the {} + move_cursor_relative(sel, 1, take_selection) - # Insert the \end{...} - view.run_command("insert", {"characters": f"\n\n\\end{{{insert_text}}}"}) + # Insert the \end{...} + text_insert = f"\n\n\\end{{{insert_text}}}" + view.run_command("insert", {"characters": text_insert}) - # Place the cursor between the \begin{...} and the \end{...} - new_region = sublime.Region(cursor + 2) - sel.clear() - sel.add(new_region) + # Place the cursor between the \begin{...} and the \end{...} + move_cursor_relative(sel, 1 - len(text_insert), take_selection) - # Add a \t for correct indentation - view.run_command("insert", {"characters": "\t"}) + # Add a \t for correct indentation + view.run_command("insert", {"characters": "\t"}) def matches_line(self, line): return bool(BEGIN_END_BEFORE_REGEX.match(line)) diff --git a/latextools/latex_fill_all.py b/latextools/latex_fill_all.py index 2237519f..355cb0c3 100644 --- a/latextools/latex_fill_all.py +++ b/latextools/latex_fill_all.py @@ -11,6 +11,7 @@ from .utils.decorators import async_completions from .utils.logging import logger from .utils.settings import get_setting +from .utils.sublime_utils import move_cursor_relative __all__ = [ "LatexFillAllEventListener", @@ -80,7 +81,7 @@ def get_completions(self, view, prefix, line): """ return None - def on_selection(self, view, insert_text): + def on_selection(self, view, insert_text, should_complete): """ Code executed after the user has selected a completion and after the completion has been inserted in the view @@ -90,8 +91,13 @@ def on_selection(self, view, insert_text): :param insert_text: Text used for the completion + :param should_complete: + Indicates if the fill helper is replacing an existing element or completing a newly created one + """ - pass + # If we add a new element, moves the cursor after the closing bracket + if should_complete: + move_cursor_relative(view.sel(), 1) def matches_line(self, line): """ @@ -1121,7 +1127,8 @@ def on_done(i, text=""): "remove_regions": self.regions_to_tuples(remove_regions), }, ) - completion_type.on_selection(view, insert_text) + + completion_type.on_selection(view, insert_text, should_complete=(insert_char != '')) # track visible input quick panels to provide key binding context VISIBLE_OVERLAYS.add(window.id()) diff --git a/latextools/utils/sublime_utils.py b/latextools/utils/sublime_utils.py index 5edb65d1..749d7efc 100644 --- a/latextools/utils/sublime_utils.py +++ b/latextools/utils/sublime_utils.py @@ -80,3 +80,28 @@ def get_project_file_name(view: sublime.View) -> str | None: if window: return window.project_file_name() return None + +def move_cursor_relative(sel, drift, take_cursor=None): + """ + Moves the cursor relatively from its current position + + :param sel: + `Selection` containing all the `Region`s to update + :param drift: + Number of character the cursor must move + :param take_cursor: + Optional array of booleans indicating if the ith `Region` must be moved. Note that take_cursor must have the same length as sel! + """ + new_regions = [] + + for i in range(len(sel)): + region = sel[i] + pos = region.end() + if take_cursor is None or take_cursor[i]: + pos = pos + drift + + new_regions.append(sublime.Region(pos)) + + sel.clear() + for r in new_regions: + sel.add(r) \ No newline at end of file From 0530a1297914d0ca251d34ce7802f327732cec97 Mon Sep 17 00:00:00 2001 From: dam024 Date: Thu, 12 Mar 2026 20:01:43 +0100 Subject: [PATCH 3/4] Add parameters to control newly added features The new features are deactivated by default --- LaTeXTools.sublime-settings | 8 ++++++++ latextools/latex_env_completions.py | 2 +- latextools/latex_fill_all.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/LaTeXTools.sublime-settings b/LaTeXTools.sublime-settings index ec1bb2ee..604366b5 100644 --- a/LaTeXTools.sublime-settings +++ b/LaTeXTools.sublime-settings @@ -101,6 +101,11 @@ // You can also use toggle: C-l,t,a,e "env_auto_trigger": false, + // Fill-helper autocompletion will close automatically an environment after \begin{. This trigger is only ran after you have selected an environment from the list + // This autoclose is only triggered when adding a new \begin{, not when modifying an existing one via the Window Command Palette + // This requires "env_auto_trigger" to be true. + "env_autoclose_trigger": false, + // Fill-helper autocompletion triggered for a wide range of references to external // files. You can also use toggle: C-l,t,a,f "fill_auto_trigger": true, @@ -120,6 +125,9 @@ // You can also use the toggle C-l,t,a,b "smart_bracket_auto_trigger": true, + // After Fill-helper autocompletion has finished his completion, moves the cursor after the closing bracket if there is one. + "smart_cursor_move_auto_trigger": false, + // ------------------------------------------------------------------ // Image Preview Settings // ------------------------------------------------------------------ diff --git a/latextools/latex_env_completions.py b/latextools/latex_env_completions.py index aebb741d..51fc43a1 100644 --- a/latextools/latex_env_completions.py +++ b/latextools/latex_env_completions.py @@ -66,7 +66,7 @@ def get_completions(self, view, prefix, line): def on_selection(self, view, insert_text, should_complete): # Do nothing the fill helper was called to replace the content of a command - if not should_complete: + if not should_complete or not get_setting("env_autoclose_trigger", False): return # The \end{...} is added only if there is a single cursor and if the 4 characters before the cursor are "\end" diff --git a/latextools/latex_fill_all.py b/latextools/latex_fill_all.py index 355cb0c3..1835907b 100644 --- a/latextools/latex_fill_all.py +++ b/latextools/latex_fill_all.py @@ -96,7 +96,7 @@ def on_selection(self, view, insert_text, should_complete): """ # If we add a new element, moves the cursor after the closing bracket - if should_complete: + if should_complete and get_setting("smart_cursor_move_auto_trigger", False): move_cursor_relative(view.sel(), 1) def matches_line(self, line): From 36e97bfe16fac850b0875c37658025349bc081c2 Mon Sep 17 00:00:00 2001 From: dam024 Date: Sat, 14 Mar 2026 13:10:51 +0100 Subject: [PATCH 4/4] Correct an indentation error when the initial \begin{...} was indented --- latextools/latex_env_completions.py | 13 +++++----- latextools/utils/sublime_utils.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/latextools/latex_env_completions.py b/latextools/latex_env_completions.py index 51fc43a1..cc85536d 100644 --- a/latextools/latex_env_completions.py +++ b/latextools/latex_env_completions.py @@ -8,7 +8,7 @@ from .utils import analysis from .utils.settings import get_setting from .utils.tex_directives import get_tex_root -from .utils.sublime_utils import move_cursor_relative +from .utils.sublime_utils import move_cursor_relative, move_cursor_vertical BEGIN_END_BEFORE_REGEX = re.compile(r"([^{}\[\]]*)\{(?:\][^{}\[\]]*\[)?(?:nigeb|dne)\\") """ @@ -72,6 +72,7 @@ def on_selection(self, view, insert_text, should_complete): # The \end{...} is added only if there is a single cursor and if the 4 characters before the cursor are "\end" sel = view.sel() take_selection = [True]*len(sel) # Indicates if we need the Region at index i will undergo autoclose of environment + indentation = [0] * len(sel) for i in range(len(sel)): cursor = sel[i].end() @@ -87,14 +88,14 @@ def on_selection(self, view, insert_text, should_complete): move_cursor_relative(sel, 1, take_selection) # Insert the \end{...} - text_insert = f"\n\n\\end{{{insert_text}}}" + text_insert = f"\n\\end{{{insert_text}}}" view.run_command("insert", {"characters": text_insert}) - # Place the cursor between the \begin{...} and the \end{...} - move_cursor_relative(sel, 1 - len(text_insert), take_selection) + # Place the cursor at the end of the line containing the \begin{...} + move_cursor_vertical(view, sel, -1, take_cursor=take_selection) - # Add a \t for correct indentation - view.run_command("insert", {"characters": "\t"}) + # Add a \n\t for correct indentation + view.run_command("insert", {"characters": "\n\t"}) def matches_line(self, line): return bool(BEGIN_END_BEFORE_REGEX.match(line)) diff --git a/latextools/utils/sublime_utils.py b/latextools/utils/sublime_utils.py index 749d7efc..76a051fe 100644 --- a/latextools/utils/sublime_utils.py +++ b/latextools/utils/sublime_utils.py @@ -87,8 +87,10 @@ def move_cursor_relative(sel, drift, take_cursor=None): :param sel: `Selection` containing all the `Region`s to update + :param drift: Number of character the cursor must move + :param take_cursor: Optional array of booleans indicating if the ith `Region` must be moved. Note that take_cursor must have the same length as sel! """ @@ -102,6 +104,43 @@ def move_cursor_relative(sel, drift, take_cursor=None): new_regions.append(sublime.Region(pos)) + sel.clear() + for r in new_regions: + sel.add(r) + +def move_cursor_vertical(view, sel, drift, take_cursor=None): + """ + Moves the cursor to the beginning of the next line or to the end of the previous line + + :param view: + Current view object + + :param sel: + `Selection` containing all the `Region`s to update + + :param drift: + +1 to go to the previous line, -1 to go to the next line + + :param take_cursor: + Optional array of booleans indicating if the ith `Region` must be moved. Note that take_cursor must have the same length as sel! + """ + new_regions = [] + + for i in range(len(sel)): + region = sel[i] + pos = region.end() + + print("Before: ", pos) + if take_cursor is None or take_cursor[i]: + # Get the current line + line = view.line(pos) + if drift > 0: + pos = line.end() + 1 + else: + pos = line.begin() - 1 + + new_regions.append(sublime.Region(pos)) + sel.clear() for r in new_regions: sel.add(r) \ No newline at end of file