From 369f0a14461719943543c394e63a258de68f0fd0 Mon Sep 17 00:00:00 2001 From: "Kay-Uwe (Kiwi) Lorenz" Date: Tue, 28 Jan 2014 04:54:23 +0100 Subject: [PATCH 001/128] ST3 compatibility, VIM compatibility --- Modelines.sublime-commands | 5 + README.rst | 34 ++++ sublime_modelines.py | 351 +++++++++++++++++++++++++++++++++++-- tests/__init__.py | 10 ++ tests/test_modelines.py | 95 ++++++++++ 5 files changed, 476 insertions(+), 19 deletions(-) create mode 100644 Modelines.sublime-commands create mode 100644 tests/__init__.py create mode 100644 tests/test_modelines.py diff --git a/Modelines.sublime-commands b/Modelines.sublime-commands new file mode 100644 index 0000000..c205086 --- /dev/null +++ b/Modelines.sublime-commands @@ -0,0 +1,5 @@ +[ + { "caption": "Modelines: Run Tests", + "command": "run_plugin_unittest", + "args": {"module": "Modelines.tests"} } +] \ No newline at end of file diff --git a/README.rst b/README.rst index 0d788ad..226c0b0 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,30 @@ one of the following syntaxes:: single-line comment character for your language. When there isn't a concept of comment, the default comment character must be used. +Vim compatibility +----------------- + +Also following modelines are supported:: + + # vim:ai:et:ts=4: + # sublime: set color_scheme="Packages/Color Scheme - Default/Monokai.tmTheme": + # sublime: set ignored_packages+=['Vintage']: + +.. note:: Modeline values are interpreted as JSON, or string as fallback. If you + use JSON Objects in modeline (like ``[]`` or ``{}``), you can only use this as + last value in a line, for quick and easy parsing reasons. + +There is full vim-modeline-compatibility implemented. Only some options are +mapped to Sublime Text options. Following options are supported so far: + +- autoindent, ai +- tabstop, ts +- expandtab, et +- syntax, syn +- number, nu + +(these are the ones, I used most) + How to Define Comment Characters in Sublime Text ------------------------------------------------ @@ -70,3 +94,13 @@ non-standard accessors as a stop-gap solution. **x_syntax** *Packages/Foo/Foo.tmLanguage* Sets the syntax to the specified *.tmLanguage* file. + + +Contributers +------------ + +Kay-Uwe (Kiwi) Lorenz (http://quelltexter.org) + - added VIM compatibility + - smart syntax matching + - modelines also parsed on save + - settings are erased from view, if removed from modeline diff --git a/sublime_modelines.py b/sublime_modelines.py index 7dc84e4..1a612a5 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -1,15 +1,169 @@ +# vim:et:ai:ts=4:syn=python: + import sublime, sublime_plugin +import re, sys, json, os + +MODELINE_PREFIX_TPL = "%s\\s*(st|sublime|vim):" -import re +MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime|vim):\x20?set\x20(.*):.*$") +MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime|vim):(.*):.*$") +KEY_VALUE = re.compile(r"""(?x) \s* + (?P\w+) \s* (?P\+?=) \s* (?P + (?: "(?:\\.|[^"\\])*" + | [\[\{].* + | [^\s:]+ + )) + """) + +KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") -MODELINE_PREFIX_TPL = "%s\\s*(st|sublime): " DEFAULT_LINE_COMMENT = '#' MULTIOPT_SEP = '; ' MAX_LINES_TO_CHECK = 50 LINE_LENGTH = 80 -MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH +ODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH + +MONITORED_OUTPUT_PANELS = ['exec'] + +ST3 = sublime.version() >= '3000' + +if ST3: + basestring = str + +VIM_MAP = { + #"gfn": "guifont" + #"guifont": {"regex": ..., 1: "font_face", 2: ("font_size", int)} + + "ts": "tabstop", + "tabstop": ("tab_size", int), + "ai": "autoindent", + "autoindent": ("auto_indent", bool), + "et": "expandtab", + "expandtab": ("translate_tabs_to_spaces", bool), + "syn": "syntax", + "syntax": ("syntax", str), + "nu": "number", + "number": ("line_numbers", bool), + # "always_show_minimap_viewport": false, + # "animation_enabled": true, + # "atomic_save": true, + # "auto_close_tags": true, + # "auto_complete": true, + # "auto_complete_commit_on_tab": false, + # "auto_complete_delay": 50, + # "auto_complete_selector": "source - comment, meta.tag - punctuation.definition.tag.begin", + # "auto_complete_size_limit": 4194304, + # "auto_complete_triggers": [ {"selector": "text.html", "characters": "<"} ], + # "auto_complete_with_fields": false, + # "auto_find_in_selection": false, + # "auto_indent": true, + # "auto_match_enabled": true, + # "binary_file_patterns": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.ttf", "*.tga", "*.dds", "*.ico", "*.eot", "*.pdf", "*.swf", "*.jar", "*.zip"], + # "bold_folder_labels": false, + # "caret_style": "smooth", + # "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", + # "copy_with_empty_selection": true, + # "default_encoding": "UTF-8", + # "default_line_ending": "system", + # "detect_indentation": true, + # "dictionary": "Packages/Language - English/en_US.dic", + # "drag_text": true, + # "draw_centered": false, + # "draw_indent_guides": true, + # "draw_minimap_border": false, + # "draw_white_space": "selection", + # "enable_hexadecimal_encoding": true, + # "enable_telemetry": "auto", + # "ensure_newline_at_eof_on_save": false, + # "fade_fold_buttons": true, + # "fallback_encoding": "Western (Windows 1252)", + # "file_exclude_patterns": ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db", "*.sublime-workspace"], + # "find_selected_text": true, + # "fold_buttons": true, + # "folder_exclude_patterns": [".svn", ".git", ".hg", "CVS"], + # "font_face": "", + # "font_options": [], # list + # "font_size": 10, + # "gpu_window_buffer": "auto", + # "gutter": true, + # "highlight_line": false, + # "highlight_modified_tabs": false, + # "ignored_packages": ["Vintage"] + # "indent_guide_options": ["draw_normal"], + # "indent_subsequent_lines": true, + # "indent_to_bracket": false, + # "index_files": true, + # "line_padding_bottom": 0, + # "line_padding_top": 0, + # "margin": 4, + # "match_brackets": true, + # "match_brackets_angle": false, + # "match_brackets_braces": true, + # "match_brackets_content": true, + # "match_brackets_square": true, + # "match_selection": true, + # "match_tags": true, + # "move_to_limit_on_up_down": false, + # "overlay_scroll_bars": "system", + # "preview_on_click": true, + # "rulers": [], # list + # "save_on_focus_lost": false, + # "scroll_past_end": true, + # "scroll_speed": 1.0, + # "shift_tab_unindent": false, + # "show_panel_on_build": true, + # "show_tab_close_buttons": true, + # "smart_indent": true, + # "spell_check": false, + # "tab_completion": true, + # "tab_size": 4, + # "theme": "Default.sublime-theme", + # "translate_tabs_to_spaces": false, + # "tree_animation_enabled": true, + # "trim_automatic_white_space": true, + # "trim_trailing_white_space_on_save": false, + # "use_simple_full_screen": false, + # "use_tab_stops": true, + # "word_separators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?", + # "word_wrap": "auto", + # "wrap_width": 0, +} + +def console_log(s, *args): + sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") + +def get_language_files(ignored_packages, *paths): + paths = list(paths) + tml_files = [] + if ST3:kj + tml_files.extend(sublime.find_resources('*.tmLanguage')) + else: + paths.insert(0, sublime.packages_path()) + + for path in paths: + for dir, dirs, files in os.walk(path): + # TODO: be sure that not tmLanguage from disabled package is taken + for fn in files: + if fn.endswith('.tmLanguage'): + tml_files.append(os.path.join(dir, fn)) + + R = re.compile("Packages[\\/]([^\\/]+)[\\/]") + result = [] + for f in tml_files: + m = R.search(f) + if m: + if m.group(1) not in ignored_packages: + result.append(f) + + return result + +def get_output_panel(name): + if ST3: + return sublime.active_window().create_output_panel(name) + else: + return sublime.active_window().get_output_panel(name) def is_modeline(prefix, line): return bool(re.match(prefix, line)) @@ -23,8 +177,13 @@ def gen_modelines(view): # There might be overlap with the top region, but it doesn't matter because # it means the buffer is tiny. bottomRegStart = filter(lambda x: x > -1, - ((view.size() - MODELINES_REG_SIZE), 0))[0] - candidates += view.lines(sublime.Region(bottomRegStart, view.size())) + ((view.size() - MODELINES_REG_SIZE), 0)) + + bottomRegStart = view.size() - MODELINES_REG_SIZE + + if bottomRegStart < 0: bottomRegStart = 0 + + candidates += view.lines( sublime.Region(bottomRegStart, view.size()) ) prefix = build_modeline_prefix(view) modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) @@ -32,9 +191,57 @@ def gen_modelines(view): for modeline in modelines: yield modeline +def vim_mapped(t, s): + if t == 'vim' or len(s) < 3: + while s in VIM_MAP: + s = VIM_MAP[s] + return s[0] + else: + return s + def gen_raw_options(modelines): + #import spdb ; spdb.start() for m in modelines: + match = MODELINE_TYPE_1.search(m) + if not match: + match = MODELINE_TYPE_2.search(m) + + if match: + type, s = match.groups() + + while True: + if s.startswith(':'): s = s[1:] + + m = KEY_VALUE.match(s) + if m: + key, op, value = m.groups() + yield vim_mapped(type, key), op, value + s = s[m.end():] + continue + + m = KEY_ONLY.match(s) + if m: + k, = m.groups() + value = "true" + + _k = vim_mapped(type, k) + if (k.startswith('no') and (type == 'vim' or ( + k[2:] in VIM_MAP or len(k) <= 4))): + + value = "false" + _k = vim_mapped(type, k[2:]) + + yield _k, '=', value + + s = s[m.end():] + continue + + break + + continue + + # original sublime modelines style opt = m.partition(':')[2].strip() if MULTIOPT_SEP in opt: for subopt in (s for s in opt.split(MULTIOPT_SEP)): @@ -46,8 +253,31 @@ def gen_raw_options(modelines): def gen_modeline_options(view): modelines = gen_modelines(view) for opt in gen_raw_options(modelines): - name, sep, value = opt.partition(' ') - yield view.settings().set, name.rstrip(':'), value.rstrip(';') + if not isinstance(opt, tuple): + #import spdb ; spdb.start() + name, sep, value = opt.partition(' ') + yield view.settings().set, name.rstrip(':'), value.rstrip(';') + + else: + name, op, value = opt + + def _setter(n,v): + if op == '+=': + if v.startswith('{'): + default = {} + elif v.startswith('['): + default = [] + elif isinstance(v, basestring): + default = "" + else: + default = 0 + + ov = view.settings().get(n, default) + v = ov + v + + view.settings().set(n,v) + + yield _setter, name, value def get_line_comment_char(view): @@ -77,13 +307,17 @@ def build_modeline_prefix(view): def to_json_type(v): """"Convert string value to proper JSON type. """ - if v.lower() in ('true', 'false'): - v = v[0].upper() + v[1:].lower() - try: - return eval(v, {}, {}) - except: - raise ValueError("Could not convert to JSON type.") + result = json.loads(v.strip()) + console_log("json: %s -> %s" % (v, repr(result))) + return result + except Exception as e: + console_log("json: %s\n" % e) + if v: + if v[0] not in "[{": + console_log("json: %s -> %s" % (v, repr(v))) + return v + raise ValueError("Could not convert from JSON: %s" % v) class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): @@ -101,19 +335,98 @@ class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): scanned. """ def do_modelines(self, view): + settings = view.settings() + + ignored_packages = settings.get('ignored_packages') + + keys = set(settings.get('sublime_modelines_keys', [])) + new_keys = set() + + base_dir = settings.get('result_base_dir') + + sys.stderr.write("do_modelines\n") + for setter, name, value in gen_modeline_options(view): - if name == 'x_syntax': - view.set_syntax_file(value) + #if 'vim' in MODELINE_PREFIX_TPL: # vimsupport + # vim_map.get(name) + console_log("modeline: %s = %s" % (name, value)) + + if name in ('x_syntax', 'syntax'): + syntax_file = None + + if os.path.isabs(value): + syntax_file = value + + if not os.path.exists(syntax_file): + console_log("%s does not exist", value) + continue + + else: + # be smart about syntax: + if base_dir: + lang_files = get_language_files(ignored_packages, base_dir) + else: + lang_files = get_language_files(ignored_packages) + + #lang_files.sort(key=lambda x: len(os.path.basename(x))) + + candidates = [] + for syntax_file in lang_files: + if value in os.path.basename(syntax_file): + candidates.append(syntax_file) + + value_lower = value.lower() + if not candidates: + for syntax_file in lang_files: + if value_lower in os.path.basename(syntax_file).lower(): + candidates.append(syntax_file) + + if not candidates: + console_log("%s cannot be resolved to a syntaxfile", value) + syntax_file = None + continue + + else: + candidates.sort(key=lambda x: len(os.path.basename(x))) + syntax_file = candidates[0] + + if ST3: + view.assign_syntax(syntax_file) + else: + view.set_syntax_file(syntax_file) + + new_keys.add('syntax') + console_log("set syntax = %s" % syntax_file) + else: try: setter(name, to_json_type(value)) - except ValueError, e: + new_keys.add(name) + except ValueError as e: sublime.status_message("[SublimeModelines] Bad modeline detected.") - print "[SublimeModelines] Bad option detected: %s, %s" % (name, value) - print "[SublimeModelines] Tip: Keys cannot be empty strings." + console_log("Bad option detected: %s, %s", name, value) + console_log("Tip: Keys cannot be empty strings.") + + for k in keys: + if k not in new_keys: + if settings.has(k): + settings.erase(k) + + settings.set('sublime_modelines_keys', list(new_keys)) + def on_load(self, view): self.do_modelines(view) def on_post_save(self, view): - self.do_modelines(view) \ No newline at end of file + self.do_modelines(view) + + if 0: + def on_modified(self, view): + for p in MONITORED_OUTPUT_PANELS: + v = get_output_panel(p) + if v.id() != view.id(): continue + return + + self.do_modelines(view) + return diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..72f1b11 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +from imp import reload + +from . import test_modelines +reload(test_modelines) + +from .. import sublime_modelines +reload(sublime_modelines) + +from .test_modelines import * + diff --git a/tests/test_modelines.py b/tests/test_modelines.py new file mode 100644 index 0000000..2737d51 --- /dev/null +++ b/tests/test_modelines.py @@ -0,0 +1,95 @@ +from sublime_unittest import TestCase +import sublime, os + + +class ModelinesTest(TestCase): + def tearDown(self): + if hasattr(self, 'tempfile'): + if os.path.exists(self.tempfile): + os.remove(self.tempfile) + + def _modeline_test(self, lines): + import tempfile + + fd, self.tempfile = mkstemp() + fd.write(lines) + fd.close() + + view = sublime.active_window().open_file(self.tempfile) + + while view.is_loading(): + yield + + # here test view's settings + + # in the end remove tempfile + + def test_modelines_1(self): + lines = ("# sublime:et:ai:ts=4:\n") + self._modeline_test(lines) + + def _gen_raw_options_test(self, line, expected): + from .. import sublime_modelines + if isinstance(line, list): + self.assertEquals([x for x in sublime_modelines.gen_raw_options(line)], expected) + else: + self.assertEquals([x for x in sublime_modelines.gen_raw_options([line])], expected) + + + def test_gen_raw_options_vim_compatibility_1(self): + self._gen_raw_options_test("# vim: set ai noet ts=4:", + + [ ('auto_indent', '=', 'true'), + ('translate_tabs_to_spaces', '=', 'false'), + ('tab_size', '=', '4') ] + ) + + def test_gen_raw_options_vim_compatibility_2(self): + self._gen_raw_options_test("# vim:ai:et:ts=4:", + [ ('auto_indent', '=', 'true'), + ('translate_tabs_to_spaces', '=', 'true'), + ('tab_size', '=', '4') ] + ) + + def test_gen_raw_options_vim_compatibility_3(self): + self._gen_raw_options_test('# sublime:ai:et:ts=4:ignored_packages+="Makefile Improved":', + [('auto_indent', '=', 'true'), + ('translate_tabs_to_spaces', '=', 'true'), + ('tab_size', '=', '4'), + ('ignored_packages', '+=', '"Makefile Improved"')] + ) + + + def test_gen_raw_options_vim_compatibility_4(self): + self._gen_raw_options_test('# sublime:ai:et:ts=4:ignored_packages+=["Makefile Improved", "Vintage"]:', + [('auto_indent', '=', 'true'), + ('translate_tabs_to_spaces', '=', 'true'), + ('tab_size', '=', '4'), + ('ignored_packages', '+=', '["Makefile Improved", "Vintage"]')] + ) + + def test_gen_raw_options_vim_compatibility_5(self): + #import spdb ; spdb.start() + self._gen_raw_options_test( + '# sublime: set color_scheme="Packages/Color Scheme - Default/Monokai.tmTheme":', + [('color_scheme', '=', '"Packages/Color Scheme - Default/Monokai.tmTheme"')]) + + + def test_gen_raw_options(self): + + mdls = [ + "# sublime: foo bar", + "# sublime: bar foo; foo bar", + "# st: baz foob", + "# st: fibz zap; zup blah", + ] + + actual = [ + "foo bar", + "bar foo", + "foo bar", + "baz foob", + "fibz zap", + "zup blah", + ] + self._gen_raw_options_test(mdls, actual) From 82618b32a552926306192799a81a18b56b341bb0 Mon Sep 17 00:00:00 2001 From: "Kay-Uwe (Kiwi) Lorenz" Date: Tue, 28 Jan 2014 04:59:33 +0100 Subject: [PATCH 002/128] removed logging --- sublime_modelines.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 1a612a5..69981c8 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -134,6 +134,10 @@ def console_log(s, *args): sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") +def debug_log(s, *args): + if 0: + sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") + def get_language_files(ignored_packages, *paths): paths = list(paths) tml_files = [] @@ -309,13 +313,10 @@ def to_json_type(v): """ try: result = json.loads(v.strip()) - console_log("json: %s -> %s" % (v, repr(result))) return result except Exception as e: - console_log("json: %s\n" % e) if v: if v[0] not in "[{": - console_log("json: %s -> %s" % (v, repr(v))) return v raise ValueError("Could not convert from JSON: %s" % v) @@ -344,12 +345,10 @@ def do_modelines(self, view): base_dir = settings.get('result_base_dir') - sys.stderr.write("do_modelines\n") - for setter, name, value in gen_modeline_options(view): #if 'vim' in MODELINE_PREFIX_TPL: # vimsupport # vim_map.get(name) - console_log("modeline: %s = %s" % (name, value)) + debug_log("modeline: %s = %s", name, value) if name in ('x_syntax', 'syntax'): syntax_file = None @@ -396,7 +395,7 @@ def do_modelines(self, view): view.set_syntax_file(syntax_file) new_keys.add('syntax') - console_log("set syntax = %s" % syntax_file) + debug_log("set syntax = %s" % syntax_file) else: try: From b858efae93f160aa97145e0df82eade5188645b1 Mon Sep 17 00:00:00 2001 From: Daniel Malon Date: Tue, 7 Oct 2014 20:49:09 +0100 Subject: [PATCH 003/128] remove typo --- sublime_modelines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 69981c8..80b1b01 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -141,7 +141,7 @@ def debug_log(s, *args): def get_language_files(ignored_packages, *paths): paths = list(paths) tml_files = [] - if ST3:kj + if ST3: tml_files.extend(sublime.find_resources('*.tmLanguage')) else: paths.insert(0, sublime.packages_path()) From d100a14078da094c7ca9f5bb54e758530de3f9f2 Mon Sep 17 00:00:00 2001 From: Daniel Malon Date: Tue, 7 Oct 2014 21:03:49 +0100 Subject: [PATCH 004/128] PY3: make python3 happy --- setup.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index d204cc0..aacde3f 100644 --- a/setup.py +++ b/setup.py @@ -59,10 +59,10 @@ def make_zipfile (base_name, base_dir, verbose=0, dry_run=0): except DistutilsExecError: # XXX really should distinguish between "couldn't find # external 'zip' command" and "zip failed". - raise DistutilsExecError, \ + raise DistutilsExecError( ("unable to create zip file '%s': " "could neither import the 'zipfile' module nor " - "find a standalone zip utility") % zip_filename + "find a standalone zip utility") % zip_filename) else: log.info("creating '%s' and adding '%s' to it", @@ -184,14 +184,14 @@ def finalize_options (self): try: self.formats = [self.default_format[os.name]] except KeyError: - raise DistutilsPlatformError, \ + raise DistutilsPlatformError( "don't know how to create source distributions " + \ - "on platform %s" % os.name + "on platform %s" % os.name) bad_format = archive_util.check_archive_formats(self.formats) if bad_format: - raise DistutilsOptionError, \ - "unknown archive format '%s'" % bad_format + raise DistutilsOptionError( + "unknown archive format '%s'" % bad_format) if self.dist_dir is None: self.dist_dir = "dist" @@ -405,7 +405,7 @@ def read_template (self): try: self.filelist.process_template_line(line) - except DistutilsTemplateError, msg: + except DistutilsTemplateError(msg): self.warn("%s, line %d: %s" % (template.filename, template.current_line, msg)) @@ -569,7 +569,7 @@ def finalize_options(self): pass def run(self): - print NotImplementedError("Command not implemented yet.") + print(NotImplementedError("Command not implemented yet.")) setup(cmdclass={'spa': spa, 'install': install}, From fbac0c32d23a06ff8b20e1c853a59d68ce2451c8 Mon Sep 17 00:00:00 2001 From: Daniel Malon Date: Tue, 7 Oct 2014 21:10:31 +0100 Subject: [PATCH 005/128] fix typo --- sublime_modelines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 80b1b01..566158b 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -22,7 +22,7 @@ MULTIOPT_SEP = '; ' MAX_LINES_TO_CHECK = 50 LINE_LENGTH = 80 -ODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH +MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH MONITORED_OUTPUT_PANELS = ['exec'] From ed26369e7cdd1dbc5a6043f33f2808b13d0d967b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:18:26 +0100 Subject: [PATCH 006/128] Migrate cleanup script to bash --- bin/CleanUp.ps1 | 7 ------- scripts/cleanup.sh | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 bin/CleanUp.ps1 create mode 100755 scripts/cleanup.sh diff --git a/bin/CleanUp.ps1 b/bin/CleanUp.ps1 deleted file mode 100644 index 8fd3ed7..0000000 --- a/bin/CleanUp.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -$script:here = split-path $MyInvocation.MyCommand.Definition -parent - -push-location "$script:here/.." - remove-item "*.pyc" -recurse -erroraction silentlycontinue - remove-item "build" -recurse -erroraction silentlycontinue - remove-item "dist" -recurse -erroraction silentlycontinue -pop-location diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh new file mode 100755 index 0000000..d988c77 --- /dev/null +++ b/scripts/cleanup.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + + +cd "$(dirname "$0")" +# Note: Though not strictly equivalent, this could also be `git clean -xffd`… +find . \( -name "*.pyc" -o -name "build" -o -name "dist" \) -exec rm -frv {} + From a2fba70744268345b2baf4bbd88163290896034e Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:21:30 +0100 Subject: [PATCH 007/128] Fix cd in cleanup --- scripts/cleanup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh index d988c77..85c228b 100755 --- a/scripts/cleanup.sh +++ b/scripts/cleanup.sh @@ -2,6 +2,6 @@ set -euo pipefail -cd "$(dirname "$0")" +cd "$(dirname "$0")/.." # Note: Though not strictly equivalent, this could also be `git clean -xffd`… find . \( -name "*.pyc" -o -name "build" -o -name "dist" \) -exec rm -frv {} + From 738e833c315df7508fc4d02cb3fe03c951cdfc47 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:21:43 +0100 Subject: [PATCH 008/128] Add __pycache__ in the cleanup files in cleanup script --- scripts/cleanup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh index 85c228b..3704be0 100755 --- a/scripts/cleanup.sh +++ b/scripts/cleanup.sh @@ -4,4 +4,4 @@ set -euo pipefail cd "$(dirname "$0")/.." # Note: Though not strictly equivalent, this could also be `git clean -xffd`… -find . \( -name "*.pyc" -o -name "build" -o -name "dist" \) -exec rm -frv {} + +find . \( -name "*.pyc" -o -name "__pycache__" -o -name "build" -o -name "dist" \) -exec rm -frv {} + From 438e47ede0255b445a4d851581ce15d926f35925 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:22:52 +0100 Subject: [PATCH 009/128] Convert .gitignore to unix EoL --- .gitignore | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index a556819..e374340 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,13 @@ -*.hgignore -*.hgtags -*.pyc -*.cache -*.sublime-project - -_*.txt -sample-grammar.js -Manifest -MANIFEST - -dist/ -build/ \ No newline at end of file +*.hgignore +*.hgtags +*.pyc +*.cache +*.sublime-project + +_*.txt +sample-grammar.js +Manifest +MANIFEST + +dist/ +build/ From 368fd092243a5bf26107901c1c6d727cb5253084 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:23:46 +0100 Subject: [PATCH 010/128] Remove obsolete hg files --- .gitignore | 2 -- .hgignore | 9 --------- .hgtags | 2 -- 3 files changed, 13 deletions(-) delete mode 100644 .hgignore delete mode 100644 .hgtags diff --git a/.gitignore b/.gitignore index e374340..7a1422d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -*.hgignore -*.hgtags *.pyc *.cache *.sublime-project diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 8896bf0..0000000 --- a/.hgignore +++ /dev/null @@ -1,9 +0,0 @@ -syntax: glob - -*.pyc -_*.txt - -MANIFEST - -build/ -dist/ \ No newline at end of file diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 5b56993..0000000 --- a/.hgtags +++ /dev/null @@ -1,2 +0,0 @@ -e4ef87463c48f5fc15b9dbe4ea2807b48ce82542 1.0 -f7da5e3a151589d7d11ee184d235f18eb77cefca 1.1 From ffd3be0112ee40e326086d2087807feb245ea1b8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 22 Jan 2026 23:49:18 +0100 Subject: [PATCH 011/128] =?UTF-8?q?Remove=20obsolete=20=E2=80=9Cmake=20rel?= =?UTF-8?q?ease=E2=80=9D=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/MakeRelease.ps1 | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 bin/MakeRelease.ps1 diff --git a/bin/MakeRelease.ps1 b/bin/MakeRelease.ps1 deleted file mode 100644 index 8d63820..0000000 --- a/bin/MakeRelease.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -$script:here = split-path $MyInvocation.MyCommand.Definition -parent -push-location "$script:here/.." - -& "$script:here/CleanUp.ps1" - -$zipExe = "$env:ProgramFiles/7-zip/7z.exe" - -& "hg" "update" "release" -& "hg" "merge" "default" -& "hg" "commit" "-m" "Merged with default." 2>&1 - -if ($rv.exception -like "*unresolved*") { - write-host "hg pull --update failed. Take a look." -foreground yellow - break -} - -$targetDir = "./dist/SublimeModelines.sublime-package" - -& "python.exe" ".\setup.py" "spa" "--no-defaults" - -(resolve-path (join-path ` - (get-location).providerpath ` - $targetDir)).path | clip.exe - -start-process chrome -arg "https://bitbucket.org/guillermooo/sublimemodelines/downloads" - -& "hg" "update" "default" -pop-location - -Write-Host "Don't forget to tag release." -foreground yellow -Write-Host "Don't forget to push to bitbucket." -foreground yellow \ No newline at end of file From 9116291212f641baac7f517c0cb06cd1986e0c94 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:24:41 +0100 Subject: [PATCH 012/128] Dummy code formatting change --- scripts/cleanup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh index 3704be0..6d0d7fd 100755 --- a/scripts/cleanup.sh +++ b/scripts/cleanup.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail +cd "$(dirname "$0")/.." -cd "$(dirname "$0")/.." # Note: Though not strictly equivalent, this could also be `git clean -xffd`… find . \( -name "*.pyc" -o -name "__pycache__" -o -name "build" -o -name "dist" \) -exec rm -frv {} + From 1ee1ba9ecd81157df216f2ef73e709b969df7aac Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:29:50 +0100 Subject: [PATCH 013/128] Remove RunTests script AFAICT the tests **must** be run in Sublime Using probably helps (and it can also run the tests in GitHub Actions). --- bin/RunTests.ps1 | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 bin/RunTests.ps1 diff --git a/bin/RunTests.ps1 b/bin/RunTests.ps1 deleted file mode 100644 index c91523a..0000000 --- a/bin/RunTests.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -# py.test.exe should discover tests autoamically without our help, but I don't -# seem to be able to get it working. -$script:here = split-path $MyInvocation.MyCommand.Definition -parent -push-location "$script:here/../tests" - -& "py.test.exe" -pop-location \ No newline at end of file From b0785da71aca0eae56351d227185875f4ca822f0 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:31:02 +0100 Subject: [PATCH 014/128] Dummy code-style fix --- tests/sublime.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/sublime.py b/tests/sublime.py index 53f04ec..4ecc364 100644 --- a/tests/sublime.py +++ b/tests/sublime.py @@ -1,4 +1,3 @@ -# #class View(object): # pass # @@ -16,4 +15,4 @@ # # #class Options(object): -# pass \ No newline at end of file +# pass From 62516f9ded5125ec1a7dbcbd3739a1d74facff02 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:42:35 +0100 Subject: [PATCH 015/128] Convert and update the Readme --- README.rst | 106 ----------------------------------------------------- Readme.md | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 106 deletions(-) delete mode 100644 README.rst create mode 100644 Readme.md diff --git a/README.rst b/README.rst deleted file mode 100644 index 226c0b0..0000000 --- a/README.rst +++ /dev/null @@ -1,106 +0,0 @@ -Sublime Modelines -================= - -Set settings local to a single buffer. A more granular approach to settings -than the per file type ``.sublime-settings`` files. - -Inspired in Vim's modelines feature. - -Getting Started -*************** - -Download and install `SublimeModelines`_. - -See the `installation instructions`_ for ``.sublime-package``\ s. - -.. _installation instructions: http://sublimetext.info/docs/en/extensibility/packages.html#installation-of-packages -.. _SublimeModelines: https://bitbucket.org/guillermooo/sublimemodelines/downloads/SublimeModelines.sublime-package - -Side Effects -************ - -Buffers will be scanned ``.on_load()`` for modelines and settings will be set -accordingly. Settings will apply **only** to the buffer declaring them. - -.. **Note**: Application- and Window-level options declared in modelines are -.. obviously global. - -Usage -***** - -How to Declare Modelines ------------------------- - -Modelines must be declared at the top or the bottom of source code files with -one of the following syntaxes:: - - # sublime: option_name value - # sublime: option_name value; another_option value; third_option value - -**Note**: ``#`` is the default comment character. Use the corresponding -single-line comment character for your language. When there isn't a concept of -comment, the default comment character must be used. - -Vim compatibility ------------------ - -Also following modelines are supported:: - - # vim:ai:et:ts=4: - # sublime: set color_scheme="Packages/Color Scheme - Default/Monokai.tmTheme": - # sublime: set ignored_packages+=['Vintage']: - -.. note:: Modeline values are interpreted as JSON, or string as fallback. If you - use JSON Objects in modeline (like ``[]`` or ``{}``), you can only use this as - last value in a line, for quick and easy parsing reasons. - -There is full vim-modeline-compatibility implemented. Only some options are -mapped to Sublime Text options. Following options are supported so far: - -- autoindent, ai -- tabstop, ts -- expandtab, et -- syntax, syn -- number, nu - -(these are the ones, I used most) - -How to Define Comment Characters in Sublime Text ------------------------------------------------- - -SublimeModelines finds the appropriate single-line comment character by inspecting -the ``shellVariables`` preference, which must be defined in a ``.tmPreferences`` -file. To see an example of how this is done, open ``Packages/Python/Miscellaneous.tmPreferences``. - -Many packages giving support for programming languages already include this, but -you might need to create a ``.tmPreferences`` file for the language you're working -with if you want SublimeModelines to be available. - - -Caveats -******* - -If the option's value contains a semicolon (``;``), make sure it isn't followed -by a blank space. Otherwise it will be interpreted as a multioption separator. - - -Non-Standard Options -******************** - -For some common cases, no directly settable option exists (for example, a -setting to specify a syntax). For such cases, Sublime Modelines provides -non-standard accessors as a stop-gap solution. - -**x_syntax** *Packages/Foo/Foo.tmLanguage* - -Sets the syntax to the specified *.tmLanguage* file. - - -Contributers ------------- - -Kay-Uwe (Kiwi) Lorenz (http://quelltexter.org) - - added VIM compatibility - - smart syntax matching - - modelines also parsed on save - - settings are erased from view, if removed from modeline diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..45d21c3 --- /dev/null +++ b/Readme.md @@ -0,0 +1,85 @@ +# Sublime Modelines + +Set settings local to a single buffer. +A more granular approach to settings than the per file type ``.sublime-settings`` files. + +Inspired by Vim’s modelines feature. + + +## Getting Started + +### Recommended Installation + +Use Package Control and install `SublimeModelines`. + +### Manual Installation + +Download and install [SublimeModelines](). + +See the [installation instructions](http://sublimetext.info/docs/en/extensibility/packages.html#installation-of-packages) for `.sublime-package`s. + + +## Side Effects + +Buffers will be scanned `.on_load()` for modelines and settings will be set accordingly. +Settings will apply **only** to the buffer declaring them. + +**Note**: Application- and window-level options declared in modelines are obviously global. + + +## Usage + +### How to Declare Modelines + +Modelines must be declared at the top or the bottom of source code files with one of the following syntaxes: + +```text +# sublime: option_name value +# sublime: option_name value; another_option value; third_option value +``` + +**Note**: +``#`` is the default comment character. +Use the corresponding single-line comment character for your language. +When there isn't a concept of comment, the default comment character must be used. + +### How to Define Comment Characters in Sublime Text + +SublimeModelines finds the appropriate single-line comment character by inspecting the `shellVariables` preference, + which must be defined in a `.tmPreferences` file. +To see an example of how this is done, open `Packages/Python/Miscellaneous.tmPreferences`. + +Many packages giving support for programming languages already include this, + but you might need to create a ``.tmPreferences`` file for the language you're working with + if you want SublimeModelines to be available. + + +## Caveats + +If the option’s value contains a semicolon (`;`), make sure it isn't followed by a blank space. +Otherwise it will be interpreted as a multi-option separator. + + +## Non-Standard Options + +For some common cases, no directly settable option exists (for example, a setting to specify a syntax). +For such cases, Sublime Modelines provides non-standard accessors as a stop-gap solution. + +```text +x_syntax Packages/Foo/Foo.tmLanguage +``` + +Sets the syntax to the specified `.tmLanguage` file. + + +# Contributers + +Kay-Uwe (Kiwi) Lorenz (): +- Added VIM compatibility; +- Smart syntax matching; +- Modelines also parsed on save; +- Settings are erased from view, if removed from modeline. + +[Frizlab](): +- Removed VIM compatibility (use `VimModelines` if you need that); +- Modernize/clean the project, and make sure it works with SublimeText 4. From 431a272af4744e62fe2b9f124088e0228b52b9e3 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:46:54 +0100 Subject: [PATCH 016/128] Update and rename the license file --- LICENSE.TXT => License.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename LICENSE.TXT => License.txt (93%) diff --git a/LICENSE.TXT b/License.txt similarity index 93% rename from LICENSE.TXT rename to License.txt index 021ec65..75c1ab3 100644 --- a/LICENSE.TXT +++ b/License.txt @@ -1,4 +1,4 @@ -Copyright (c) 2010 Guillermo López-Anglada +Copyright (c) 2026 Frizlab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. From 767ae64291df272729936d18925055fb0d6486ab Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 00:57:38 +0100 Subject: [PATCH 017/128] Convert EoL to unix everywhere --- License.txt | 38 +- MANIFEST.in | 6 +- Readme.md | 170 ++--- setup.py | 1164 +++++++++++++++---------------- sublime_modelines.py | 862 +++++++++++------------ tests/sublime.py | 36 +- tests/sublime_plugin.py | 34 +- tests/test_sublime_modelines.py | 312 ++++----- 8 files changed, 1311 insertions(+), 1311 deletions(-) diff --git a/License.txt b/License.txt index 75c1ab3..e43064d 100644 --- a/License.txt +++ b/License.txt @@ -1,19 +1,19 @@ -Copyright (c) 2026 Frizlab - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +Copyright (c) 2026 Frizlab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index fa6606a..e87ca7a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include sublime_modelines.py -include LICENSE.TXT -include README.rst +include sublime_modelines.py +include LICENSE.TXT +include README.rst prune setup.py \ No newline at end of file diff --git a/Readme.md b/Readme.md index 45d21c3..e281cfe 100644 --- a/Readme.md +++ b/Readme.md @@ -1,85 +1,85 @@ -# Sublime Modelines - -Set settings local to a single buffer. -A more granular approach to settings than the per file type ``.sublime-settings`` files. - -Inspired by Vim’s modelines feature. - - -## Getting Started - -### Recommended Installation - -Use Package Control and install `SublimeModelines`. - -### Manual Installation - -Download and install [SublimeModelines](). - -See the [installation instructions](http://sublimetext.info/docs/en/extensibility/packages.html#installation-of-packages) for `.sublime-package`s. - - -## Side Effects - -Buffers will be scanned `.on_load()` for modelines and settings will be set accordingly. -Settings will apply **only** to the buffer declaring them. - -**Note**: Application- and window-level options declared in modelines are obviously global. - - -## Usage - -### How to Declare Modelines - -Modelines must be declared at the top or the bottom of source code files with one of the following syntaxes: - -```text -# sublime: option_name value -# sublime: option_name value; another_option value; third_option value -``` - -**Note**: -``#`` is the default comment character. -Use the corresponding single-line comment character for your language. -When there isn't a concept of comment, the default comment character must be used. - -### How to Define Comment Characters in Sublime Text - -SublimeModelines finds the appropriate single-line comment character by inspecting the `shellVariables` preference, - which must be defined in a `.tmPreferences` file. -To see an example of how this is done, open `Packages/Python/Miscellaneous.tmPreferences`. - -Many packages giving support for programming languages already include this, - but you might need to create a ``.tmPreferences`` file for the language you're working with - if you want SublimeModelines to be available. - - -## Caveats - -If the option’s value contains a semicolon (`;`), make sure it isn't followed by a blank space. -Otherwise it will be interpreted as a multi-option separator. - - -## Non-Standard Options - -For some common cases, no directly settable option exists (for example, a setting to specify a syntax). -For such cases, Sublime Modelines provides non-standard accessors as a stop-gap solution. - -```text -x_syntax Packages/Foo/Foo.tmLanguage -``` - -Sets the syntax to the specified `.tmLanguage` file. - - -# Contributers - -Kay-Uwe (Kiwi) Lorenz (): -- Added VIM compatibility; -- Smart syntax matching; -- Modelines also parsed on save; -- Settings are erased from view, if removed from modeline. - -[Frizlab](): -- Removed VIM compatibility (use `VimModelines` if you need that); -- Modernize/clean the project, and make sure it works with SublimeText 4. +# Sublime Modelines + +Set settings local to a single buffer. +A more granular approach to settings than the per file type ``.sublime-settings`` files. + +Inspired by Vim’s modelines feature. + + +## Getting Started + +### Recommended Installation + +Use Package Control and install `SublimeModelines`. + +### Manual Installation + +Download and install [SublimeModelines](). + +See the [installation instructions](http://sublimetext.info/docs/en/extensibility/packages.html#installation-of-packages) for `.sublime-package`s. + + +## Side Effects + +Buffers will be scanned `.on_load()` for modelines and settings will be set accordingly. +Settings will apply **only** to the buffer declaring them. + +**Note**: Application- and window-level options declared in modelines are obviously global. + + +## Usage + +### How to Declare Modelines + +Modelines must be declared at the top or the bottom of source code files with one of the following syntaxes: + +```text +# sublime: option_name value +# sublime: option_name value; another_option value; third_option value +``` + +**Note**: +``#`` is the default comment character. +Use the corresponding single-line comment character for your language. +When there isn't a concept of comment, the default comment character must be used. + +### How to Define Comment Characters in Sublime Text + +SublimeModelines finds the appropriate single-line comment character by inspecting the `shellVariables` preference, + which must be defined in a `.tmPreferences` file. +To see an example of how this is done, open `Packages/Python/Miscellaneous.tmPreferences`. + +Many packages giving support for programming languages already include this, + but you might need to create a ``.tmPreferences`` file for the language you're working with + if you want SublimeModelines to be available. + + +## Caveats + +If the option’s value contains a semicolon (`;`), make sure it isn't followed by a blank space. +Otherwise it will be interpreted as a multi-option separator. + + +## Non-Standard Options + +For some common cases, no directly settable option exists (for example, a setting to specify a syntax). +For such cases, Sublime Modelines provides non-standard accessors as a stop-gap solution. + +```text +x_syntax Packages/Foo/Foo.tmLanguage +``` + +Sets the syntax to the specified `.tmLanguage` file. + + +# Contributers + +Kay-Uwe (Kiwi) Lorenz (): +- Added VIM compatibility; +- Smart syntax matching; +- Modelines also parsed on save; +- Settings are erased from view, if removed from modeline. + +[Frizlab](): +- Removed VIM compatibility (use `VimModelines` if you need that); +- Modernize/clean the project, and make sure it works with SublimeText 4. diff --git a/setup.py b/setup.py index aacde3f..1180c02 100644 --- a/setup.py +++ b/setup.py @@ -1,583 +1,583 @@ -# -*- coding: utf-8 -*- - -"""Commands to build and manage .sublime-package archives with distutils.""" - -import os - -from distutils.core import Command -from distutils.filelist import FileList -from distutils.text_file import TextFile -from distutils import dir_util, dep_util, file_util, archive_util -from distutils import log -from distutils.core import setup -from distutils.errors import * - - -import os, string -import sys -from types import * -from glob import glob -from distutils.core import Command -from distutils import dir_util, dep_util, file_util, archive_util -from distutils.text_file import TextFile -from distutils.errors import * -from distutils.filelist import FileList -from distutils import log - -import os -from distutils.errors import DistutilsExecError -from distutils.spawn import spawn -from distutils.dir_util import mkpath -from distutils import log - -def make_zipfile (base_name, base_dir, verbose=0, dry_run=0): - """Create a zip file from all the files under 'base_dir'. The output - zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" - Python module (if available) or the InfoZIP "zip" utility (if installed - and found on the default search path). If neither tool is available, - raises DistutilsExecError. Returns the name of the output zip file. - """ - try: - import zipfile - except ImportError: - zipfile = None - - zip_filename = base_name + ".sublime-package" - mkpath(os.path.dirname(zip_filename), dry_run=dry_run) - - # If zipfile module is not available, try spawning an external - # 'zip' command. - if zipfile is None: - if verbose: - zipoptions = "-r" - else: - zipoptions = "-rq" - - try: - spawn(["zip", zipoptions, zip_filename, base_dir], - dry_run=dry_run) - except DistutilsExecError: - # XXX really should distinguish between "couldn't find - # external 'zip' command" and "zip failed". - raise DistutilsExecError( - ("unable to create zip file '%s': " - "could neither import the 'zipfile' module nor " - "find a standalone zip utility") % zip_filename) - - else: - log.info("creating '%s' and adding '%s' to it", - zip_filename, base_dir) - - if not dry_run: - z = zipfile.ZipFile(zip_filename, "w", - compression=zipfile.ZIP_DEFLATED) - - for dirpath, dirnames, filenames in os.walk(base_dir): - for name in filenames: - path = os.path.normpath(os.path.join(dirpath, name)) - if dirpath == base_dir: - arcname = name - else: - arcname = path - if os.path.isfile(path): - z.write(path, arcname) - log.info("adding '%s'" % path) - z.close() - - return zip_filename - - -def show_formats (): - """Print all possible values for the 'formats' option (used by - the "--help-formats" command-line option). - """ - from distutils.fancy_getopt import FancyGetopt - from distutils.archive_util import ARCHIVE_FORMATS - formats=[] - for format in ARCHIVE_FORMATS.keys(): - formats.append(("formats=" + format, None, - ARCHIVE_FORMATS[format][2])) - formats.sort() - pretty_printer = FancyGetopt(formats) - pretty_printer.print_help( - "List of available source distribution formats:") - -class spa (Command): - - description = "create a source distribution (tarball, zip file, etc.)" - - user_options = [ - ('template=', 't', - "name of manifest template file [default: MANIFEST.in]"), - ('manifest=', 'm', - "name of manifest file [default: MANIFEST]"), - ('use-defaults', None, - "include the default file set in the manifest " - "[default; disable with --no-defaults]"), - ('no-defaults', None, - "don't include the default file set"), - ('prune', None, - "specifically exclude files/directories that should not be " - "distributed (build tree, RCS/CVS dirs, etc.) " - "[default; disable with --no-prune]"), - ('no-prune', None, - "don't automatically exclude anything"), - ('manifest-only', 'o', - "just regenerate the manifest and then stop " - "(implies --force-manifest)"), - ('force-manifest', 'f', - "forcibly regenerate the manifest and carry on as usual"), - ('formats=', None, - "formats for source distribution (comma-separated list)"), - ('keep-temp', 'k', - "keep the distribution tree around after creating " + - "archive file(s)"), - ('dist-dir=', 'd', - "directory to put the source distribution archive(s) in " - "[default: dist]"), - ] - - boolean_options = ['use-defaults', 'prune', - 'manifest-only', 'force-manifest', - 'keep-temp'] - - help_options = [ - ('help-formats', None, - "list available distribution formats", show_formats), - ] - - negative_opt = {'no-defaults': 'use-defaults', - 'no-prune': 'prune' } - - default_format = { 'posix': 'gztar', - 'nt': 'zip' } - - def initialize_options (self): - # 'template' and 'manifest' are, respectively, the names of - # the manifest template and manifest file. - self.template = None - self.manifest = None - - # 'use_defaults': if true, we will include the default file set - # in the manifest - self.use_defaults = 1 - self.prune = 1 - - self.manifest_only = 0 - self.force_manifest = 0 - - self.formats = None - self.keep_temp = 0 - self.dist_dir = None - - self.archive_files = None - - - def finalize_options (self): - if self.manifest is None: - self.manifest = "MANIFEST" - if self.template is None: - self.template = "MANIFEST.in" - - self.ensure_string_list('formats') - if self.formats is None: - try: - self.formats = [self.default_format[os.name]] - except KeyError: - raise DistutilsPlatformError( - "don't know how to create source distributions " + \ - "on platform %s" % os.name) - - bad_format = archive_util.check_archive_formats(self.formats) - if bad_format: - raise DistutilsOptionError( - "unknown archive format '%s'" % bad_format) - - if self.dist_dir is None: - self.dist_dir = "dist" - - - def run (self): - - # 'filelist' contains the list of files that will make up the - # manifest - self.filelist = FileList() - - # Ensure that all required meta-data is given; warn if not (but - # don't die, it's not *that* serious!) - self.check_metadata() - - # Do whatever it takes to get the list of files to process - # (process the manifest template, read an existing manifest, - # whatever). File list is accumulated in 'self.filelist'. - self.get_file_list() - - # If user just wanted us to regenerate the manifest, stop now. - if self.manifest_only: - return - - # Otherwise, go ahead and create the source distribution tarball, - # or zipfile, or whatever. - self.make_distribution() - - - def check_metadata (self): - """Ensure that all required elements of meta-data (name, version, - URL, (author and author_email) or (maintainer and - maintainer_email)) are supplied by the Distribution object; warn if - any are missing. - """ - metadata = self.distribution.metadata - - missing = [] - for attr in ('name', 'version', 'url'): - if not (hasattr(metadata, attr) and getattr(metadata, attr)): - missing.append(attr) - - if missing: - self.warn("missing required meta-data: " + - string.join(missing, ", ")) - - if metadata.author: - if not metadata.author_email: - self.warn("missing meta-data: if 'author' supplied, " + - "'author_email' must be supplied too") - elif metadata.maintainer: - if not metadata.maintainer_email: - self.warn("missing meta-data: if 'maintainer' supplied, " + - "'maintainer_email' must be supplied too") - else: - self.warn("missing meta-data: either (author and author_email) " + - "or (maintainer and maintainer_email) " + - "must be supplied") - - # check_metadata () - - - def get_file_list (self): - """Figure out the list of files to include in the source - distribution, and put it in 'self.filelist'. This might involve - reading the manifest template (and writing the manifest), or just - reading the manifest, or just using the default file set -- it all - depends on the user's options and the state of the filesystem. - """ - - # If we have a manifest template, see if it's newer than the - # manifest; if so, we'll regenerate the manifest. - template_exists = os.path.isfile(self.template) - if template_exists: - template_newer = dep_util.newer(self.template, self.manifest) - - # The contents of the manifest file almost certainly depend on the - # setup script as well as the manifest template -- so if the setup - # script is newer than the manifest, we'll regenerate the manifest - # from the template. (Well, not quite: if we already have a - # manifest, but there's no template -- which will happen if the - # developer elects to generate a manifest some other way -- then we - # can't regenerate the manifest, so we don't.) - self.debug_print("checking if %s newer than %s" % - (self.distribution.script_name, self.manifest)) - setup_newer = dep_util.newer(self.distribution.script_name, - self.manifest) - - # cases: - # 1) no manifest, template exists: generate manifest - # (covered by 2a: no manifest == template newer) - # 2) manifest & template exist: - # 2a) template or setup script newer than manifest: - # regenerate manifest - # 2b) manifest newer than both: - # do nothing (unless --force or --manifest-only) - # 3) manifest exists, no template: - # do nothing (unless --force or --manifest-only) - # 4) no manifest, no template: generate w/ warning ("defaults only") - - manifest_outofdate = (template_exists and - (template_newer or setup_newer)) - force_regen = self.force_manifest or self.manifest_only - manifest_exists = os.path.isfile(self.manifest) - neither_exists = (not template_exists and not manifest_exists) - - # Regenerate the manifest if necessary (or if explicitly told to) - if manifest_outofdate or neither_exists or force_regen: - if not template_exists: - self.warn(("manifest template '%s' does not exist " + - "(using default file list)") % - self.template) - self.filelist.findall() - - if self.use_defaults: - self.add_defaults() - if template_exists: - self.read_template() - if self.prune: - self.prune_file_list() - - self.filelist.sort() - self.filelist.remove_duplicates() - self.write_manifest() - - # Don't regenerate the manifest, just read it in. - else: - self.read_manifest() - - # get_file_list () - - - def add_defaults (self): - """Add all the default files to self.filelist: - - README or README.txt - - setup.py - - test/test*.py - - all pure Python modules mentioned in setup script - - all C sources listed as part of extensions or C libraries - in the setup script (doesn't catch C headers!) - Warns if (README or README.txt) or setup.py are missing; everything - else is optional. - """ - - standards = [('README', 'README.txt'), self.distribution.script_name] - for fn in standards: - # XXX - if fn == 'setup.py': continue # We don't want setup.py - if type(fn) is TupleType: - alts = fn - got_it = 0 - for fn in alts: - if os.path.exists(fn): - got_it = 1 - self.filelist.append(fn) - break - - if not got_it: - self.warn("standard file not found: should have one of " + - string.join(alts, ', ')) - else: - if os.path.exists(fn): - self.filelist.append(fn) - else: - self.warn("standard file '%s' not found" % fn) - - optional = ['test/test*.py', 'setup.cfg'] - for pattern in optional: - files = filter(os.path.isfile, glob(pattern)) - if files: - self.filelist.extend(files) - - if self.distribution.has_pure_modules(): - build_py = self.get_finalized_command('build_py') - self.filelist.extend(build_py.get_source_files()) - - if self.distribution.has_ext_modules(): - build_ext = self.get_finalized_command('build_ext') - self.filelist.extend(build_ext.get_source_files()) - - if self.distribution.has_c_libraries(): - build_clib = self.get_finalized_command('build_clib') - self.filelist.extend(build_clib.get_source_files()) - - if self.distribution.has_scripts(): - build_scripts = self.get_finalized_command('build_scripts') - self.filelist.extend(build_scripts.get_source_files()) - - # add_defaults () - - - def read_template (self): - """Read and parse manifest template file named by self.template. - - (usually "MANIFEST.in") The parsing and processing is done by - 'self.filelist', which updates itself accordingly. - """ - log.info("reading manifest template '%s'", self.template) - template = TextFile(self.template, - strip_comments=1, - skip_blanks=1, - join_lines=1, - lstrip_ws=1, - rstrip_ws=1, - collapse_join=1) - - while 1: - line = template.readline() - if line is None: # end of file - break - - try: - self.filelist.process_template_line(line) - except DistutilsTemplateError(msg): - self.warn("%s, line %d: %s" % (template.filename, - template.current_line, - msg)) - - # read_template () - - - def prune_file_list (self): - """Prune off branches that might slip into the file list as created - by 'read_template()', but really don't belong there: - * the build tree (typically "build") - * the release tree itself (only an issue if we ran "spa" - previously with --keep-temp, or it aborted) - * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories - """ - build = self.get_finalized_command('build') - base_dir = self.distribution.get_fullname() - - self.filelist.exclude_pattern(None, prefix=build.build_base) - self.filelist.exclude_pattern(None, prefix=base_dir) - - # pruning out vcs directories - # both separators are used under win32 - if sys.platform == 'win32': - seps = r'/|\\' - else: - seps = '/' - - vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', - '_darcs'] - vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps) - self.filelist.exclude_pattern(vcs_ptrn, is_regex=1) - - def write_manifest (self): - """Write the file list in 'self.filelist' (presumably as filled in - by 'add_defaults()' and 'read_template()') to the manifest file - named by 'self.manifest'. - """ - self.execute(file_util.write_file, - (self.manifest, self.filelist.files), - "writing manifest file '%s'" % self.manifest) - - # write_manifest () - - - def read_manifest (self): - """Read the manifest file (named by 'self.manifest') and use it to - fill in 'self.filelist', the list of files to include in the source - distribution. - """ - log.info("reading manifest file '%s'", self.manifest) - manifest = open(self.manifest) - while 1: - line = manifest.readline() - if line == '': # end of file - break - if line[-1] == '\n': - line = line[0:-1] - self.filelist.append(line) - manifest.close() - - # read_manifest () - - - def make_release_tree (self, base_dir, files): - """Create the directory tree that will become the source - distribution archive. All directories implied by the filenames in - 'files' are created under 'base_dir', and then we hard link or copy - (if hard linking is unavailable) those files into place. - Essentially, this duplicates the developer's source tree, but in a - directory named after the distribution, containing only the files - to be distributed. - """ - # Create all the directories under 'base_dir' necessary to - # put 'files' there; the 'mkpath()' is just so we don't die - # if the manifest happens to be empty. - self.mkpath(base_dir) - dir_util.create_tree(base_dir, files, dry_run=self.dry_run) - - # And walk over the list of files, either making a hard link (if - # os.link exists) to each one that doesn't already exist in its - # corresponding location under 'base_dir', or copying each file - # that's out-of-date in 'base_dir'. (Usually, all files will be - # out-of-date, because by default we blow away 'base_dir' when - # we're done making the distribution archives.) - - if hasattr(os, 'link'): # can make hard links on this system - link = 'hard' - msg = "making hard links in %s..." % base_dir - else: # nope, have to copy - link = None - msg = "copying files to %s..." % base_dir - - if not files: - log.warn("no files to distribute -- empty manifest?") - else: - log.info(msg) - for file in files: - if not os.path.isfile(file): - log.warn("'%s' not a regular file -- skipping" % file) - else: - dest = os.path.join(base_dir, file) - self.copy_file(file, dest, link=link) - - self.distribution.metadata.write_pkg_info(base_dir) - - # make_release_tree () - - def make_distribution (self): - """Create the source distribution(s). First, we create the release - tree with 'make_release_tree()'; then, we create all required - archive files (according to 'self.formats') from the release tree. - Finally, we clean up by blowing away the release tree (unless - 'self.keep_temp' is true). The list of archive files created is - stored so it can be retrieved later by 'get_archive_files()'. - """ - # Don't warn about missing meta-data here -- should be (and is!) - # done elsewhere. - # base_dir = self.distribution.get_fullname() - base_dir = self.distribution.get_name() - # XXX - base_dir = base_dir - base_name = os.path.join(self.dist_dir, base_dir) - - - self.make_release_tree(base_dir, self.filelist.files) - archive_files = [] # remember names of files we create - # tar archive must be created last to avoid overwrite and remove - if 'tar' in self.formats: - self.formats.append(self.formats.pop(self.formats.index('tar'))) - - for fmt in self.formats: - # file = self.make_archive(base_name, fmt, base_dir=base_dir) - file = make_zipfile(base_name, base_dir=base_dir) - archive_files.append(file) - self.distribution.dist_files.append(('spa', '', file)) - - self.archive_files = archive_files - - if not self.keep_temp: - dir_util.remove_tree(base_dir, dry_run=self.dry_run) - - def get_archive_files (self): - """Return the list of archive files created when the command - was run, or None if the command hasn't run yet. - """ - return self.archive_files - -# class spa - - -class install(Command): - """Does it make sense?""" - - user_options = [('aa', 'a', 'aa')] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - print(NotImplementedError("Command not implemented yet.")) - - -setup(cmdclass={'spa': spa, 'install': install}, - name='SublimeModelines', - version='1.1', - description='Vim-like modelines for Sublime Text.', - author='Guillermo López-Anglada', - author_email='guillermo@sublimetext.info', - url='http://sublimetext.info', - py_modules=['sublime_modelines.py'] +# -*- coding: utf-8 -*- + +"""Commands to build and manage .sublime-package archives with distutils.""" + +import os + +from distutils.core import Command +from distutils.filelist import FileList +from distutils.text_file import TextFile +from distutils import dir_util, dep_util, file_util, archive_util +from distutils import log +from distutils.core import setup +from distutils.errors import * + + +import os, string +import sys +from types import * +from glob import glob +from distutils.core import Command +from distutils import dir_util, dep_util, file_util, archive_util +from distutils.text_file import TextFile +from distutils.errors import * +from distutils.filelist import FileList +from distutils import log + +import os +from distutils.errors import DistutilsExecError +from distutils.spawn import spawn +from distutils.dir_util import mkpath +from distutils import log + +def make_zipfile (base_name, base_dir, verbose=0, dry_run=0): + """Create a zip file from all the files under 'base_dir'. The output + zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" + Python module (if available) or the InfoZIP "zip" utility (if installed + and found on the default search path). If neither tool is available, + raises DistutilsExecError. Returns the name of the output zip file. + """ + try: + import zipfile + except ImportError: + zipfile = None + + zip_filename = base_name + ".sublime-package" + mkpath(os.path.dirname(zip_filename), dry_run=dry_run) + + # If zipfile module is not available, try spawning an external + # 'zip' command. + if zipfile is None: + if verbose: + zipoptions = "-r" + else: + zipoptions = "-rq" + + try: + spawn(["zip", zipoptions, zip_filename, base_dir], + dry_run=dry_run) + except DistutilsExecError: + # XXX really should distinguish between "couldn't find + # external 'zip' command" and "zip failed". + raise DistutilsExecError( + ("unable to create zip file '%s': " + "could neither import the 'zipfile' module nor " + "find a standalone zip utility") % zip_filename) + + else: + log.info("creating '%s' and adding '%s' to it", + zip_filename, base_dir) + + if not dry_run: + z = zipfile.ZipFile(zip_filename, "w", + compression=zipfile.ZIP_DEFLATED) + + for dirpath, dirnames, filenames in os.walk(base_dir): + for name in filenames: + path = os.path.normpath(os.path.join(dirpath, name)) + if dirpath == base_dir: + arcname = name + else: + arcname = path + if os.path.isfile(path): + z.write(path, arcname) + log.info("adding '%s'" % path) + z.close() + + return zip_filename + + +def show_formats (): + """Print all possible values for the 'formats' option (used by + the "--help-formats" command-line option). + """ + from distutils.fancy_getopt import FancyGetopt + from distutils.archive_util import ARCHIVE_FORMATS + formats=[] + for format in ARCHIVE_FORMATS.keys(): + formats.append(("formats=" + format, None, + ARCHIVE_FORMATS[format][2])) + formats.sort() + pretty_printer = FancyGetopt(formats) + pretty_printer.print_help( + "List of available source distribution formats:") + +class spa (Command): + + description = "create a source distribution (tarball, zip file, etc.)" + + user_options = [ + ('template=', 't', + "name of manifest template file [default: MANIFEST.in]"), + ('manifest=', 'm', + "name of manifest file [default: MANIFEST]"), + ('use-defaults', None, + "include the default file set in the manifest " + "[default; disable with --no-defaults]"), + ('no-defaults', None, + "don't include the default file set"), + ('prune', None, + "specifically exclude files/directories that should not be " + "distributed (build tree, RCS/CVS dirs, etc.) " + "[default; disable with --no-prune]"), + ('no-prune', None, + "don't automatically exclude anything"), + ('manifest-only', 'o', + "just regenerate the manifest and then stop " + "(implies --force-manifest)"), + ('force-manifest', 'f', + "forcibly regenerate the manifest and carry on as usual"), + ('formats=', None, + "formats for source distribution (comma-separated list)"), + ('keep-temp', 'k', + "keep the distribution tree around after creating " + + "archive file(s)"), + ('dist-dir=', 'd', + "directory to put the source distribution archive(s) in " + "[default: dist]"), + ] + + boolean_options = ['use-defaults', 'prune', + 'manifest-only', 'force-manifest', + 'keep-temp'] + + help_options = [ + ('help-formats', None, + "list available distribution formats", show_formats), + ] + + negative_opt = {'no-defaults': 'use-defaults', + 'no-prune': 'prune' } + + default_format = { 'posix': 'gztar', + 'nt': 'zip' } + + def initialize_options (self): + # 'template' and 'manifest' are, respectively, the names of + # the manifest template and manifest file. + self.template = None + self.manifest = None + + # 'use_defaults': if true, we will include the default file set + # in the manifest + self.use_defaults = 1 + self.prune = 1 + + self.manifest_only = 0 + self.force_manifest = 0 + + self.formats = None + self.keep_temp = 0 + self.dist_dir = None + + self.archive_files = None + + + def finalize_options (self): + if self.manifest is None: + self.manifest = "MANIFEST" + if self.template is None: + self.template = "MANIFEST.in" + + self.ensure_string_list('formats') + if self.formats is None: + try: + self.formats = [self.default_format[os.name]] + except KeyError: + raise DistutilsPlatformError( + "don't know how to create source distributions " + \ + "on platform %s" % os.name) + + bad_format = archive_util.check_archive_formats(self.formats) + if bad_format: + raise DistutilsOptionError( + "unknown archive format '%s'" % bad_format) + + if self.dist_dir is None: + self.dist_dir = "dist" + + + def run (self): + + # 'filelist' contains the list of files that will make up the + # manifest + self.filelist = FileList() + + # Ensure that all required meta-data is given; warn if not (but + # don't die, it's not *that* serious!) + self.check_metadata() + + # Do whatever it takes to get the list of files to process + # (process the manifest template, read an existing manifest, + # whatever). File list is accumulated in 'self.filelist'. + self.get_file_list() + + # If user just wanted us to regenerate the manifest, stop now. + if self.manifest_only: + return + + # Otherwise, go ahead and create the source distribution tarball, + # or zipfile, or whatever. + self.make_distribution() + + + def check_metadata (self): + """Ensure that all required elements of meta-data (name, version, + URL, (author and author_email) or (maintainer and + maintainer_email)) are supplied by the Distribution object; warn if + any are missing. + """ + metadata = self.distribution.metadata + + missing = [] + for attr in ('name', 'version', 'url'): + if not (hasattr(metadata, attr) and getattr(metadata, attr)): + missing.append(attr) + + if missing: + self.warn("missing required meta-data: " + + string.join(missing, ", ")) + + if metadata.author: + if not metadata.author_email: + self.warn("missing meta-data: if 'author' supplied, " + + "'author_email' must be supplied too") + elif metadata.maintainer: + if not metadata.maintainer_email: + self.warn("missing meta-data: if 'maintainer' supplied, " + + "'maintainer_email' must be supplied too") + else: + self.warn("missing meta-data: either (author and author_email) " + + "or (maintainer and maintainer_email) " + + "must be supplied") + + # check_metadata () + + + def get_file_list (self): + """Figure out the list of files to include in the source + distribution, and put it in 'self.filelist'. This might involve + reading the manifest template (and writing the manifest), or just + reading the manifest, or just using the default file set -- it all + depends on the user's options and the state of the filesystem. + """ + + # If we have a manifest template, see if it's newer than the + # manifest; if so, we'll regenerate the manifest. + template_exists = os.path.isfile(self.template) + if template_exists: + template_newer = dep_util.newer(self.template, self.manifest) + + # The contents of the manifest file almost certainly depend on the + # setup script as well as the manifest template -- so if the setup + # script is newer than the manifest, we'll regenerate the manifest + # from the template. (Well, not quite: if we already have a + # manifest, but there's no template -- which will happen if the + # developer elects to generate a manifest some other way -- then we + # can't regenerate the manifest, so we don't.) + self.debug_print("checking if %s newer than %s" % + (self.distribution.script_name, self.manifest)) + setup_newer = dep_util.newer(self.distribution.script_name, + self.manifest) + + # cases: + # 1) no manifest, template exists: generate manifest + # (covered by 2a: no manifest == template newer) + # 2) manifest & template exist: + # 2a) template or setup script newer than manifest: + # regenerate manifest + # 2b) manifest newer than both: + # do nothing (unless --force or --manifest-only) + # 3) manifest exists, no template: + # do nothing (unless --force or --manifest-only) + # 4) no manifest, no template: generate w/ warning ("defaults only") + + manifest_outofdate = (template_exists and + (template_newer or setup_newer)) + force_regen = self.force_manifest or self.manifest_only + manifest_exists = os.path.isfile(self.manifest) + neither_exists = (not template_exists and not manifest_exists) + + # Regenerate the manifest if necessary (or if explicitly told to) + if manifest_outofdate or neither_exists or force_regen: + if not template_exists: + self.warn(("manifest template '%s' does not exist " + + "(using default file list)") % + self.template) + self.filelist.findall() + + if self.use_defaults: + self.add_defaults() + if template_exists: + self.read_template() + if self.prune: + self.prune_file_list() + + self.filelist.sort() + self.filelist.remove_duplicates() + self.write_manifest() + + # Don't regenerate the manifest, just read it in. + else: + self.read_manifest() + + # get_file_list () + + + def add_defaults (self): + """Add all the default files to self.filelist: + - README or README.txt + - setup.py + - test/test*.py + - all pure Python modules mentioned in setup script + - all C sources listed as part of extensions or C libraries + in the setup script (doesn't catch C headers!) + Warns if (README or README.txt) or setup.py are missing; everything + else is optional. + """ + + standards = [('README', 'README.txt'), self.distribution.script_name] + for fn in standards: + # XXX + if fn == 'setup.py': continue # We don't want setup.py + if type(fn) is TupleType: + alts = fn + got_it = 0 + for fn in alts: + if os.path.exists(fn): + got_it = 1 + self.filelist.append(fn) + break + + if not got_it: + self.warn("standard file not found: should have one of " + + string.join(alts, ', ')) + else: + if os.path.exists(fn): + self.filelist.append(fn) + else: + self.warn("standard file '%s' not found" % fn) + + optional = ['test/test*.py', 'setup.cfg'] + for pattern in optional: + files = filter(os.path.isfile, glob(pattern)) + if files: + self.filelist.extend(files) + + if self.distribution.has_pure_modules(): + build_py = self.get_finalized_command('build_py') + self.filelist.extend(build_py.get_source_files()) + + if self.distribution.has_ext_modules(): + build_ext = self.get_finalized_command('build_ext') + self.filelist.extend(build_ext.get_source_files()) + + if self.distribution.has_c_libraries(): + build_clib = self.get_finalized_command('build_clib') + self.filelist.extend(build_clib.get_source_files()) + + if self.distribution.has_scripts(): + build_scripts = self.get_finalized_command('build_scripts') + self.filelist.extend(build_scripts.get_source_files()) + + # add_defaults () + + + def read_template (self): + """Read and parse manifest template file named by self.template. + + (usually "MANIFEST.in") The parsing and processing is done by + 'self.filelist', which updates itself accordingly. + """ + log.info("reading manifest template '%s'", self.template) + template = TextFile(self.template, + strip_comments=1, + skip_blanks=1, + join_lines=1, + lstrip_ws=1, + rstrip_ws=1, + collapse_join=1) + + while 1: + line = template.readline() + if line is None: # end of file + break + + try: + self.filelist.process_template_line(line) + except DistutilsTemplateError(msg): + self.warn("%s, line %d: %s" % (template.filename, + template.current_line, + msg)) + + # read_template () + + + def prune_file_list (self): + """Prune off branches that might slip into the file list as created + by 'read_template()', but really don't belong there: + * the build tree (typically "build") + * the release tree itself (only an issue if we ran "spa" + previously with --keep-temp, or it aborted) + * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories + """ + build = self.get_finalized_command('build') + base_dir = self.distribution.get_fullname() + + self.filelist.exclude_pattern(None, prefix=build.build_base) + self.filelist.exclude_pattern(None, prefix=base_dir) + + # pruning out vcs directories + # both separators are used under win32 + if sys.platform == 'win32': + seps = r'/|\\' + else: + seps = '/' + + vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', + '_darcs'] + vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps) + self.filelist.exclude_pattern(vcs_ptrn, is_regex=1) + + def write_manifest (self): + """Write the file list in 'self.filelist' (presumably as filled in + by 'add_defaults()' and 'read_template()') to the manifest file + named by 'self.manifest'. + """ + self.execute(file_util.write_file, + (self.manifest, self.filelist.files), + "writing manifest file '%s'" % self.manifest) + + # write_manifest () + + + def read_manifest (self): + """Read the manifest file (named by 'self.manifest') and use it to + fill in 'self.filelist', the list of files to include in the source + distribution. + """ + log.info("reading manifest file '%s'", self.manifest) + manifest = open(self.manifest) + while 1: + line = manifest.readline() + if line == '': # end of file + break + if line[-1] == '\n': + line = line[0:-1] + self.filelist.append(line) + manifest.close() + + # read_manifest () + + + def make_release_tree (self, base_dir, files): + """Create the directory tree that will become the source + distribution archive. All directories implied by the filenames in + 'files' are created under 'base_dir', and then we hard link or copy + (if hard linking is unavailable) those files into place. + Essentially, this duplicates the developer's source tree, but in a + directory named after the distribution, containing only the files + to be distributed. + """ + # Create all the directories under 'base_dir' necessary to + # put 'files' there; the 'mkpath()' is just so we don't die + # if the manifest happens to be empty. + self.mkpath(base_dir) + dir_util.create_tree(base_dir, files, dry_run=self.dry_run) + + # And walk over the list of files, either making a hard link (if + # os.link exists) to each one that doesn't already exist in its + # corresponding location under 'base_dir', or copying each file + # that's out-of-date in 'base_dir'. (Usually, all files will be + # out-of-date, because by default we blow away 'base_dir' when + # we're done making the distribution archives.) + + if hasattr(os, 'link'): # can make hard links on this system + link = 'hard' + msg = "making hard links in %s..." % base_dir + else: # nope, have to copy + link = None + msg = "copying files to %s..." % base_dir + + if not files: + log.warn("no files to distribute -- empty manifest?") + else: + log.info(msg) + for file in files: + if not os.path.isfile(file): + log.warn("'%s' not a regular file -- skipping" % file) + else: + dest = os.path.join(base_dir, file) + self.copy_file(file, dest, link=link) + + self.distribution.metadata.write_pkg_info(base_dir) + + # make_release_tree () + + def make_distribution (self): + """Create the source distribution(s). First, we create the release + tree with 'make_release_tree()'; then, we create all required + archive files (according to 'self.formats') from the release tree. + Finally, we clean up by blowing away the release tree (unless + 'self.keep_temp' is true). The list of archive files created is + stored so it can be retrieved later by 'get_archive_files()'. + """ + # Don't warn about missing meta-data here -- should be (and is!) + # done elsewhere. + # base_dir = self.distribution.get_fullname() + base_dir = self.distribution.get_name() + # XXX + base_dir = base_dir + base_name = os.path.join(self.dist_dir, base_dir) + + + self.make_release_tree(base_dir, self.filelist.files) + archive_files = [] # remember names of files we create + # tar archive must be created last to avoid overwrite and remove + if 'tar' in self.formats: + self.formats.append(self.formats.pop(self.formats.index('tar'))) + + for fmt in self.formats: + # file = self.make_archive(base_name, fmt, base_dir=base_dir) + file = make_zipfile(base_name, base_dir=base_dir) + archive_files.append(file) + self.distribution.dist_files.append(('spa', '', file)) + + self.archive_files = archive_files + + if not self.keep_temp: + dir_util.remove_tree(base_dir, dry_run=self.dry_run) + + def get_archive_files (self): + """Return the list of archive files created when the command + was run, or None if the command hasn't run yet. + """ + return self.archive_files + +# class spa + + +class install(Command): + """Does it make sense?""" + + user_options = [('aa', 'a', 'aa')] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + print(NotImplementedError("Command not implemented yet.")) + + +setup(cmdclass={'spa': spa, 'install': install}, + name='SublimeModelines', + version='1.1', + description='Vim-like modelines for Sublime Text.', + author='Guillermo López-Anglada', + author_email='guillermo@sublimetext.info', + url='http://sublimetext.info', + py_modules=['sublime_modelines.py'] ) \ No newline at end of file diff --git a/sublime_modelines.py b/sublime_modelines.py index 566158b..0c49c7d 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -1,431 +1,431 @@ -# vim:et:ai:ts=4:syn=python: - -import sublime, sublime_plugin -import re, sys, json, os - -MODELINE_PREFIX_TPL = "%s\\s*(st|sublime|vim):" - -MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime|vim):\x20?set\x20(.*):.*$") -MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime|vim):(.*):.*$") - -KEY_VALUE = re.compile(r"""(?x) \s* - (?P\w+) \s* (?P\+?=) \s* (?P - (?: "(?:\\.|[^"\\])*" - | [\[\{].* - | [^\s:]+ - )) - """) - -KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") - -DEFAULT_LINE_COMMENT = '#' -MULTIOPT_SEP = '; ' -MAX_LINES_TO_CHECK = 50 -LINE_LENGTH = 80 -MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH - -MONITORED_OUTPUT_PANELS = ['exec'] - -ST3 = sublime.version() >= '3000' - -if ST3: - basestring = str - -VIM_MAP = { - #"gfn": "guifont" - #"guifont": {"regex": ..., 1: "font_face", 2: ("font_size", int)} - - "ts": "tabstop", - "tabstop": ("tab_size", int), - "ai": "autoindent", - "autoindent": ("auto_indent", bool), - "et": "expandtab", - "expandtab": ("translate_tabs_to_spaces", bool), - "syn": "syntax", - "syntax": ("syntax", str), - "nu": "number", - "number": ("line_numbers", bool), - - # "always_show_minimap_viewport": false, - # "animation_enabled": true, - # "atomic_save": true, - # "auto_close_tags": true, - # "auto_complete": true, - # "auto_complete_commit_on_tab": false, - # "auto_complete_delay": 50, - # "auto_complete_selector": "source - comment, meta.tag - punctuation.definition.tag.begin", - # "auto_complete_size_limit": 4194304, - # "auto_complete_triggers": [ {"selector": "text.html", "characters": "<"} ], - # "auto_complete_with_fields": false, - # "auto_find_in_selection": false, - # "auto_indent": true, - # "auto_match_enabled": true, - # "binary_file_patterns": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.ttf", "*.tga", "*.dds", "*.ico", "*.eot", "*.pdf", "*.swf", "*.jar", "*.zip"], - # "bold_folder_labels": false, - # "caret_style": "smooth", - # "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", - # "copy_with_empty_selection": true, - # "default_encoding": "UTF-8", - # "default_line_ending": "system", - # "detect_indentation": true, - # "dictionary": "Packages/Language - English/en_US.dic", - # "drag_text": true, - # "draw_centered": false, - # "draw_indent_guides": true, - # "draw_minimap_border": false, - # "draw_white_space": "selection", - # "enable_hexadecimal_encoding": true, - # "enable_telemetry": "auto", - # "ensure_newline_at_eof_on_save": false, - # "fade_fold_buttons": true, - # "fallback_encoding": "Western (Windows 1252)", - # "file_exclude_patterns": ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db", "*.sublime-workspace"], - # "find_selected_text": true, - # "fold_buttons": true, - # "folder_exclude_patterns": [".svn", ".git", ".hg", "CVS"], - # "font_face": "", - # "font_options": [], # list - # "font_size": 10, - # "gpu_window_buffer": "auto", - # "gutter": true, - # "highlight_line": false, - # "highlight_modified_tabs": false, - # "ignored_packages": ["Vintage"] - # "indent_guide_options": ["draw_normal"], - # "indent_subsequent_lines": true, - # "indent_to_bracket": false, - # "index_files": true, - # "line_padding_bottom": 0, - # "line_padding_top": 0, - # "margin": 4, - # "match_brackets": true, - # "match_brackets_angle": false, - # "match_brackets_braces": true, - # "match_brackets_content": true, - # "match_brackets_square": true, - # "match_selection": true, - # "match_tags": true, - # "move_to_limit_on_up_down": false, - # "overlay_scroll_bars": "system", - # "preview_on_click": true, - # "rulers": [], # list - # "save_on_focus_lost": false, - # "scroll_past_end": true, - # "scroll_speed": 1.0, - # "shift_tab_unindent": false, - # "show_panel_on_build": true, - # "show_tab_close_buttons": true, - # "smart_indent": true, - # "spell_check": false, - # "tab_completion": true, - # "tab_size": 4, - # "theme": "Default.sublime-theme", - # "translate_tabs_to_spaces": false, - # "tree_animation_enabled": true, - # "trim_automatic_white_space": true, - # "trim_trailing_white_space_on_save": false, - # "use_simple_full_screen": false, - # "use_tab_stops": true, - # "word_separators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?", - # "word_wrap": "auto", - # "wrap_width": 0, -} - -def console_log(s, *args): - sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") - -def debug_log(s, *args): - if 0: - sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") - -def get_language_files(ignored_packages, *paths): - paths = list(paths) - tml_files = [] - if ST3: - tml_files.extend(sublime.find_resources('*.tmLanguage')) - else: - paths.insert(0, sublime.packages_path()) - - for path in paths: - for dir, dirs, files in os.walk(path): - # TODO: be sure that not tmLanguage from disabled package is taken - for fn in files: - if fn.endswith('.tmLanguage'): - tml_files.append(os.path.join(dir, fn)) - - R = re.compile("Packages[\\/]([^\\/]+)[\\/]") - result = [] - for f in tml_files: - m = R.search(f) - if m: - if m.group(1) not in ignored_packages: - result.append(f) - - return result - -def get_output_panel(name): - if ST3: - return sublime.active_window().create_output_panel(name) - else: - return sublime.active_window().get_output_panel(name) - -def is_modeline(prefix, line): - return bool(re.match(prefix, line)) - - -def gen_modelines(view): - topRegEnd = min(MODELINES_REG_SIZE, view.size()) - candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) - - # Consider modelines at the end of the buffer too. - # There might be overlap with the top region, but it doesn't matter because - # it means the buffer is tiny. - bottomRegStart = filter(lambda x: x > -1, - ((view.size() - MODELINES_REG_SIZE), 0)) - - bottomRegStart = view.size() - MODELINES_REG_SIZE - - if bottomRegStart < 0: bottomRegStart = 0 - - candidates += view.lines( sublime.Region(bottomRegStart, view.size()) ) - - prefix = build_modeline_prefix(view) - modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) - - for modeline in modelines: - yield modeline - -def vim_mapped(t, s): - if t == 'vim' or len(s) < 3: - while s in VIM_MAP: - s = VIM_MAP[s] - return s[0] - else: - return s - - -def gen_raw_options(modelines): - #import spdb ; spdb.start() - for m in modelines: - match = MODELINE_TYPE_1.search(m) - if not match: - match = MODELINE_TYPE_2.search(m) - - if match: - type, s = match.groups() - - while True: - if s.startswith(':'): s = s[1:] - - m = KEY_VALUE.match(s) - if m: - key, op, value = m.groups() - yield vim_mapped(type, key), op, value - s = s[m.end():] - continue - - m = KEY_ONLY.match(s) - if m: - k, = m.groups() - value = "true" - - _k = vim_mapped(type, k) - if (k.startswith('no') and (type == 'vim' or ( - k[2:] in VIM_MAP or len(k) <= 4))): - - value = "false" - _k = vim_mapped(type, k[2:]) - - yield _k, '=', value - - s = s[m.end():] - continue - - break - - continue - - # original sublime modelines style - opt = m.partition(':')[2].strip() - if MULTIOPT_SEP in opt: - for subopt in (s for s in opt.split(MULTIOPT_SEP)): - yield subopt - else: - yield opt - - -def gen_modeline_options(view): - modelines = gen_modelines(view) - for opt in gen_raw_options(modelines): - if not isinstance(opt, tuple): - #import spdb ; spdb.start() - name, sep, value = opt.partition(' ') - yield view.settings().set, name.rstrip(':'), value.rstrip(';') - - else: - name, op, value = opt - - def _setter(n,v): - if op == '+=': - if v.startswith('{'): - default = {} - elif v.startswith('['): - default = [] - elif isinstance(v, basestring): - default = "" - else: - default = 0 - - ov = view.settings().get(n, default) - v = ov + v - - view.settings().set(n,v) - - yield _setter, name, value - - -def get_line_comment_char(view): - commentChar = "" - commentChar2 = "" - try: - for pair in view.meta_info("shellVariables", 0): - if pair["name"] == "TM_COMMENT_START": - commentChar = pair["value"] - if pair["name"] == "TM_COMMENT_START_2": - commentChar2 = pair["value"] - if commentChar and commentChar2: - break - except TypeError: - pass - - if not commentChar2: - return re.escape(commentChar.strip()) - else: - return "(" + re.escape(commentChar.strip()) + "|" + re.escape(commentChar2.strip()) + ")" - -def build_modeline_prefix(view): - lineComment = get_line_comment_char(view).lstrip() or DEFAULT_LINE_COMMENT - return (MODELINE_PREFIX_TPL % lineComment) - - -def to_json_type(v): - """"Convert string value to proper JSON type. - """ - try: - result = json.loads(v.strip()) - return result - except Exception as e: - if v: - if v[0] not in "[{": - return v - raise ValueError("Could not convert from JSON: %s" % v) - - -class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): - """This plugin provides a feature similar to vim modelines. - Modelines set options local to the view by declaring them in the - source code file itself. - - Example: - mysourcecodefile.py - # sublime: gutter false - # sublime: translate_tab_to_spaces true - - The top as well as the bottom of the buffer is scanned for modelines. - MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be - scanned. - """ - def do_modelines(self, view): - settings = view.settings() - - ignored_packages = settings.get('ignored_packages') - - keys = set(settings.get('sublime_modelines_keys', [])) - new_keys = set() - - base_dir = settings.get('result_base_dir') - - for setter, name, value in gen_modeline_options(view): - #if 'vim' in MODELINE_PREFIX_TPL: # vimsupport - # vim_map.get(name) - debug_log("modeline: %s = %s", name, value) - - if name in ('x_syntax', 'syntax'): - syntax_file = None - - if os.path.isabs(value): - syntax_file = value - - if not os.path.exists(syntax_file): - console_log("%s does not exist", value) - continue - - else: - # be smart about syntax: - if base_dir: - lang_files = get_language_files(ignored_packages, base_dir) - else: - lang_files = get_language_files(ignored_packages) - - #lang_files.sort(key=lambda x: len(os.path.basename(x))) - - candidates = [] - for syntax_file in lang_files: - if value in os.path.basename(syntax_file): - candidates.append(syntax_file) - - value_lower = value.lower() - if not candidates: - for syntax_file in lang_files: - if value_lower in os.path.basename(syntax_file).lower(): - candidates.append(syntax_file) - - if not candidates: - console_log("%s cannot be resolved to a syntaxfile", value) - syntax_file = None - continue - - else: - candidates.sort(key=lambda x: len(os.path.basename(x))) - syntax_file = candidates[0] - - if ST3: - view.assign_syntax(syntax_file) - else: - view.set_syntax_file(syntax_file) - - new_keys.add('syntax') - debug_log("set syntax = %s" % syntax_file) - - else: - try: - setter(name, to_json_type(value)) - new_keys.add(name) - except ValueError as e: - sublime.status_message("[SublimeModelines] Bad modeline detected.") - console_log("Bad option detected: %s, %s", name, value) - console_log("Tip: Keys cannot be empty strings.") - - for k in keys: - if k not in new_keys: - if settings.has(k): - settings.erase(k) - - settings.set('sublime_modelines_keys', list(new_keys)) - - - def on_load(self, view): - self.do_modelines(view) - - def on_post_save(self, view): - self.do_modelines(view) - - if 0: - def on_modified(self, view): - for p in MONITORED_OUTPUT_PANELS: - v = get_output_panel(p) - if v.id() != view.id(): continue - return - - self.do_modelines(view) - return +# vim:et:ai:ts=4:syn=python: + +import sublime, sublime_plugin +import re, sys, json, os + +MODELINE_PREFIX_TPL = "%s\\s*(st|sublime|vim):" + +MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime|vim):\x20?set\x20(.*):.*$") +MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime|vim):(.*):.*$") + +KEY_VALUE = re.compile(r"""(?x) \s* + (?P\w+) \s* (?P\+?=) \s* (?P + (?: "(?:\\.|[^"\\])*" + | [\[\{].* + | [^\s:]+ + )) + """) + +KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") + +DEFAULT_LINE_COMMENT = '#' +MULTIOPT_SEP = '; ' +MAX_LINES_TO_CHECK = 50 +LINE_LENGTH = 80 +MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH + +MONITORED_OUTPUT_PANELS = ['exec'] + +ST3 = sublime.version() >= '3000' + +if ST3: + basestring = str + +VIM_MAP = { + #"gfn": "guifont" + #"guifont": {"regex": ..., 1: "font_face", 2: ("font_size", int)} + + "ts": "tabstop", + "tabstop": ("tab_size", int), + "ai": "autoindent", + "autoindent": ("auto_indent", bool), + "et": "expandtab", + "expandtab": ("translate_tabs_to_spaces", bool), + "syn": "syntax", + "syntax": ("syntax", str), + "nu": "number", + "number": ("line_numbers", bool), + + # "always_show_minimap_viewport": false, + # "animation_enabled": true, + # "atomic_save": true, + # "auto_close_tags": true, + # "auto_complete": true, + # "auto_complete_commit_on_tab": false, + # "auto_complete_delay": 50, + # "auto_complete_selector": "source - comment, meta.tag - punctuation.definition.tag.begin", + # "auto_complete_size_limit": 4194304, + # "auto_complete_triggers": [ {"selector": "text.html", "characters": "<"} ], + # "auto_complete_with_fields": false, + # "auto_find_in_selection": false, + # "auto_indent": true, + # "auto_match_enabled": true, + # "binary_file_patterns": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.ttf", "*.tga", "*.dds", "*.ico", "*.eot", "*.pdf", "*.swf", "*.jar", "*.zip"], + # "bold_folder_labels": false, + # "caret_style": "smooth", + # "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", + # "copy_with_empty_selection": true, + # "default_encoding": "UTF-8", + # "default_line_ending": "system", + # "detect_indentation": true, + # "dictionary": "Packages/Language - English/en_US.dic", + # "drag_text": true, + # "draw_centered": false, + # "draw_indent_guides": true, + # "draw_minimap_border": false, + # "draw_white_space": "selection", + # "enable_hexadecimal_encoding": true, + # "enable_telemetry": "auto", + # "ensure_newline_at_eof_on_save": false, + # "fade_fold_buttons": true, + # "fallback_encoding": "Western (Windows 1252)", + # "file_exclude_patterns": ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db", "*.sublime-workspace"], + # "find_selected_text": true, + # "fold_buttons": true, + # "folder_exclude_patterns": [".svn", ".git", ".hg", "CVS"], + # "font_face": "", + # "font_options": [], # list + # "font_size": 10, + # "gpu_window_buffer": "auto", + # "gutter": true, + # "highlight_line": false, + # "highlight_modified_tabs": false, + # "ignored_packages": ["Vintage"] + # "indent_guide_options": ["draw_normal"], + # "indent_subsequent_lines": true, + # "indent_to_bracket": false, + # "index_files": true, + # "line_padding_bottom": 0, + # "line_padding_top": 0, + # "margin": 4, + # "match_brackets": true, + # "match_brackets_angle": false, + # "match_brackets_braces": true, + # "match_brackets_content": true, + # "match_brackets_square": true, + # "match_selection": true, + # "match_tags": true, + # "move_to_limit_on_up_down": false, + # "overlay_scroll_bars": "system", + # "preview_on_click": true, + # "rulers": [], # list + # "save_on_focus_lost": false, + # "scroll_past_end": true, + # "scroll_speed": 1.0, + # "shift_tab_unindent": false, + # "show_panel_on_build": true, + # "show_tab_close_buttons": true, + # "smart_indent": true, + # "spell_check": false, + # "tab_completion": true, + # "tab_size": 4, + # "theme": "Default.sublime-theme", + # "translate_tabs_to_spaces": false, + # "tree_animation_enabled": true, + # "trim_automatic_white_space": true, + # "trim_trailing_white_space_on_save": false, + # "use_simple_full_screen": false, + # "use_tab_stops": true, + # "word_separators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?", + # "word_wrap": "auto", + # "wrap_width": 0, +} + +def console_log(s, *args): + sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") + +def debug_log(s, *args): + if 0: + sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") + +def get_language_files(ignored_packages, *paths): + paths = list(paths) + tml_files = [] + if ST3: + tml_files.extend(sublime.find_resources('*.tmLanguage')) + else: + paths.insert(0, sublime.packages_path()) + + for path in paths: + for dir, dirs, files in os.walk(path): + # TODO: be sure that not tmLanguage from disabled package is taken + for fn in files: + if fn.endswith('.tmLanguage'): + tml_files.append(os.path.join(dir, fn)) + + R = re.compile("Packages[\\/]([^\\/]+)[\\/]") + result = [] + for f in tml_files: + m = R.search(f) + if m: + if m.group(1) not in ignored_packages: + result.append(f) + + return result + +def get_output_panel(name): + if ST3: + return sublime.active_window().create_output_panel(name) + else: + return sublime.active_window().get_output_panel(name) + +def is_modeline(prefix, line): + return bool(re.match(prefix, line)) + + +def gen_modelines(view): + topRegEnd = min(MODELINES_REG_SIZE, view.size()) + candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) + + # Consider modelines at the end of the buffer too. + # There might be overlap with the top region, but it doesn't matter because + # it means the buffer is tiny. + bottomRegStart = filter(lambda x: x > -1, + ((view.size() - MODELINES_REG_SIZE), 0)) + + bottomRegStart = view.size() - MODELINES_REG_SIZE + + if bottomRegStart < 0: bottomRegStart = 0 + + candidates += view.lines( sublime.Region(bottomRegStart, view.size()) ) + + prefix = build_modeline_prefix(view) + modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) + + for modeline in modelines: + yield modeline + +def vim_mapped(t, s): + if t == 'vim' or len(s) < 3: + while s in VIM_MAP: + s = VIM_MAP[s] + return s[0] + else: + return s + + +def gen_raw_options(modelines): + #import spdb ; spdb.start() + for m in modelines: + match = MODELINE_TYPE_1.search(m) + if not match: + match = MODELINE_TYPE_2.search(m) + + if match: + type, s = match.groups() + + while True: + if s.startswith(':'): s = s[1:] + + m = KEY_VALUE.match(s) + if m: + key, op, value = m.groups() + yield vim_mapped(type, key), op, value + s = s[m.end():] + continue + + m = KEY_ONLY.match(s) + if m: + k, = m.groups() + value = "true" + + _k = vim_mapped(type, k) + if (k.startswith('no') and (type == 'vim' or ( + k[2:] in VIM_MAP or len(k) <= 4))): + + value = "false" + _k = vim_mapped(type, k[2:]) + + yield _k, '=', value + + s = s[m.end():] + continue + + break + + continue + + # original sublime modelines style + opt = m.partition(':')[2].strip() + if MULTIOPT_SEP in opt: + for subopt in (s for s in opt.split(MULTIOPT_SEP)): + yield subopt + else: + yield opt + + +def gen_modeline_options(view): + modelines = gen_modelines(view) + for opt in gen_raw_options(modelines): + if not isinstance(opt, tuple): + #import spdb ; spdb.start() + name, sep, value = opt.partition(' ') + yield view.settings().set, name.rstrip(':'), value.rstrip(';') + + else: + name, op, value = opt + + def _setter(n,v): + if op == '+=': + if v.startswith('{'): + default = {} + elif v.startswith('['): + default = [] + elif isinstance(v, basestring): + default = "" + else: + default = 0 + + ov = view.settings().get(n, default) + v = ov + v + + view.settings().set(n,v) + + yield _setter, name, value + + +def get_line_comment_char(view): + commentChar = "" + commentChar2 = "" + try: + for pair in view.meta_info("shellVariables", 0): + if pair["name"] == "TM_COMMENT_START": + commentChar = pair["value"] + if pair["name"] == "TM_COMMENT_START_2": + commentChar2 = pair["value"] + if commentChar and commentChar2: + break + except TypeError: + pass + + if not commentChar2: + return re.escape(commentChar.strip()) + else: + return "(" + re.escape(commentChar.strip()) + "|" + re.escape(commentChar2.strip()) + ")" + +def build_modeline_prefix(view): + lineComment = get_line_comment_char(view).lstrip() or DEFAULT_LINE_COMMENT + return (MODELINE_PREFIX_TPL % lineComment) + + +def to_json_type(v): + """"Convert string value to proper JSON type. + """ + try: + result = json.loads(v.strip()) + return result + except Exception as e: + if v: + if v[0] not in "[{": + return v + raise ValueError("Could not convert from JSON: %s" % v) + + +class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): + """This plugin provides a feature similar to vim modelines. + Modelines set options local to the view by declaring them in the + source code file itself. + + Example: + mysourcecodefile.py + # sublime: gutter false + # sublime: translate_tab_to_spaces true + + The top as well as the bottom of the buffer is scanned for modelines. + MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be + scanned. + """ + def do_modelines(self, view): + settings = view.settings() + + ignored_packages = settings.get('ignored_packages') + + keys = set(settings.get('sublime_modelines_keys', [])) + new_keys = set() + + base_dir = settings.get('result_base_dir') + + for setter, name, value in gen_modeline_options(view): + #if 'vim' in MODELINE_PREFIX_TPL: # vimsupport + # vim_map.get(name) + debug_log("modeline: %s = %s", name, value) + + if name in ('x_syntax', 'syntax'): + syntax_file = None + + if os.path.isabs(value): + syntax_file = value + + if not os.path.exists(syntax_file): + console_log("%s does not exist", value) + continue + + else: + # be smart about syntax: + if base_dir: + lang_files = get_language_files(ignored_packages, base_dir) + else: + lang_files = get_language_files(ignored_packages) + + #lang_files.sort(key=lambda x: len(os.path.basename(x))) + + candidates = [] + for syntax_file in lang_files: + if value in os.path.basename(syntax_file): + candidates.append(syntax_file) + + value_lower = value.lower() + if not candidates: + for syntax_file in lang_files: + if value_lower in os.path.basename(syntax_file).lower(): + candidates.append(syntax_file) + + if not candidates: + console_log("%s cannot be resolved to a syntaxfile", value) + syntax_file = None + continue + + else: + candidates.sort(key=lambda x: len(os.path.basename(x))) + syntax_file = candidates[0] + + if ST3: + view.assign_syntax(syntax_file) + else: + view.set_syntax_file(syntax_file) + + new_keys.add('syntax') + debug_log("set syntax = %s" % syntax_file) + + else: + try: + setter(name, to_json_type(value)) + new_keys.add(name) + except ValueError as e: + sublime.status_message("[SublimeModelines] Bad modeline detected.") + console_log("Bad option detected: %s, %s", name, value) + console_log("Tip: Keys cannot be empty strings.") + + for k in keys: + if k not in new_keys: + if settings.has(k): + settings.erase(k) + + settings.set('sublime_modelines_keys', list(new_keys)) + + + def on_load(self, view): + self.do_modelines(view) + + def on_post_save(self, view): + self.do_modelines(view) + + if 0: + def on_modified(self, view): + for p in MONITORED_OUTPUT_PANELS: + v = get_output_panel(p) + if v.id() != view.id(): continue + return + + self.do_modelines(view) + return diff --git a/tests/sublime.py b/tests/sublime.py index 4ecc364..798b76a 100644 --- a/tests/sublime.py +++ b/tests/sublime.py @@ -1,18 +1,18 @@ -#class View(object): -# pass -# -# -#class RegionSet(object): -# pass -# -# -#class Region(object): -# pass -# -# -#class Window(object): -# pass -# -# -#class Options(object): -# pass +#class View(object): +# pass +# +# +#class RegionSet(object): +# pass +# +# +#class Region(object): +# pass +# +# +#class Window(object): +# pass +# +# +#class Options(object): +# pass diff --git a/tests/sublime_plugin.py b/tests/sublime_plugin.py index 4e09c54..ce340de 100644 --- a/tests/sublime_plugin.py +++ b/tests/sublime_plugin.py @@ -1,18 +1,18 @@ -class Plugin(object): - pass - - -class ApplicationCommand(Plugin): - pass - - -class WindowCommand(Plugin): - pass - - -class TextCommand(Plugin): - pass - - -class EventListener(Plugin): +class Plugin(object): + pass + + +class ApplicationCommand(Plugin): + pass + + +class WindowCommand(Plugin): + pass + + +class TextCommand(Plugin): + pass + + +class EventListener(Plugin): pass \ No newline at end of file diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 536f1fd..d63bf5a 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,157 +1,157 @@ -import unittest -import sys -import os - -import mock - -import sublime - - -sys.path.extend([".."]) - -sublime.packagesPath = mock.Mock() -sublime.packagesPath.return_value = "XXX" - - -import sublime_plugin -import sublime_modelines - - -def pytest_funcarg__view(request): - view = mock.Mock() - return view - - -def test_get_line_comment_char_Does_meta_info_GetCorrectArgs(view): - sublime_modelines.get_line_comment_char(view) - - actual = view.meta_info.call_args - expected = (("shellVariables", 0), {}) - - assert actual == expected - - -def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(view): - view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "#"}] - - expected = "#" - actual = sublime_modelines.get_line_comment_char(view) - - assert expected == actual - - -def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(view): - view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] - - expected = "" - actual = sublime_modelines.get_line_comment_char(view) - - assert expected == actual - - -def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(view): - view.meta_info.return_value = None - - expected = "" - actual = sublime_modelines.get_line_comment_char(view) - - assert expected == actual - - -def test_build_modeline_prefix_AreDefaultsCorrect(): - actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT - expected = "%s\\s*(st|sublime): " % "TEST", "#" - assert actual == expected - - -def test_BuildPrefixWithDynamicLineCommentChar(view): - view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "//"}] - expected = "%s\\s*(st|sublime): " % "//" - actual = sublime_modelines.build_modeline_prefix(view) - assert actual == expected - - -def test_BuildPrefixWithDefaultLineCommentChar(view): - view.meta_info.return_value = None - - expected = "%s\\s*(st|sublime): " % "#" - actual = sublime_modelines.build_modeline_prefix(view) - - assert expected == actual - - -def test_gen_modelines(view): - sublime.Region = mock.Mock() - view.substr.side_effect = lambda x: x - view.size.return_value = 0 - view.lines.return_value = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline", - "random stuff" - ] - modelines = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline" - ] * 2 # the buffer is so small that there's overlap top/bottom modelines. - - assert modelines == [l for l in sublime_modelines.gen_modelines(view)] - - -def test_gen_raw_options(): - mdls = [ - "# sublime: foo bar", - "# sublime: bar foo; foo bar", - "# st: baz foob", - "# st: fibz zap; zup blah" - ] - - actual = [ - "foo bar", - "bar foo", - "foo bar", - "baz foob", - "fibz zap", - "zup blah", - ] - - assert actual == [x for x in sublime_modelines.gen_raw_options(mdls)] - - -def test_gen_modeline_options(view): - set = view.settings().set - - gen_modelines = mock.Mock() - gen_modelines.return_value = ["# sublime: foo bar", - "# sublime: baz zoom"] - - gen_raw_options = mock.Mock() - gen_raw_options.return_value = ["foo bar", - "baz zoom"] - - sublime_modelines.gen_modelines = gen_modelines - sublime_modelines.gen_raw_options = gen_raw_options - - actual = [x for x in sublime_modelines.gen_modeline_options(view)] - assert [(set, "foo", "bar"), (set, "baz", "zoom")] == actual - - -def test_is_modeline(view): - sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") - view.substr.return_value = "# sublime: " - assert sublime_modelines.is_modeline(view, 0) - - -def test_to_json_type(): - a = "1" - b = "1.0" - c = "false" - d = "true" - e = list() - - assert sublime_modelines.to_json_type(a) == 1 - assert sublime_modelines.to_json_type(b) == 1.0 - assert sublime_modelines.to_json_type(c) == False - assert sublime_modelines.to_json_type(d) == True +import unittest +import sys +import os + +import mock + +import sublime + + +sys.path.extend([".."]) + +sublime.packagesPath = mock.Mock() +sublime.packagesPath.return_value = "XXX" + + +import sublime_plugin +import sublime_modelines + + +def pytest_funcarg__view(request): + view = mock.Mock() + return view + + +def test_get_line_comment_char_Does_meta_info_GetCorrectArgs(view): + sublime_modelines.get_line_comment_char(view) + + actual = view.meta_info.call_args + expected = (("shellVariables", 0), {}) + + assert actual == expected + + +def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(view): + view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "#"}] + + expected = "#" + actual = sublime_modelines.get_line_comment_char(view) + + assert expected == actual + + +def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(view): + view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] + + expected = "" + actual = sublime_modelines.get_line_comment_char(view) + + assert expected == actual + + +def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(view): + view.meta_info.return_value = None + + expected = "" + actual = sublime_modelines.get_line_comment_char(view) + + assert expected == actual + + +def test_build_modeline_prefix_AreDefaultsCorrect(): + actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT + expected = "%s\\s*(st|sublime): " % "TEST", "#" + assert actual == expected + + +def test_BuildPrefixWithDynamicLineCommentChar(view): + view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "//"}] + expected = "%s\\s*(st|sublime): " % "//" + actual = sublime_modelines.build_modeline_prefix(view) + assert actual == expected + + +def test_BuildPrefixWithDefaultLineCommentChar(view): + view.meta_info.return_value = None + + expected = "%s\\s*(st|sublime): " % "#" + actual = sublime_modelines.build_modeline_prefix(view) + + assert expected == actual + + +def test_gen_modelines(view): + sublime.Region = mock.Mock() + view.substr.side_effect = lambda x: x + view.size.return_value = 0 + view.lines.return_value = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline", + "random stuff" + ] + modelines = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline" + ] * 2 # the buffer is so small that there's overlap top/bottom modelines. + + assert modelines == [l for l in sublime_modelines.gen_modelines(view)] + + +def test_gen_raw_options(): + mdls = [ + "# sublime: foo bar", + "# sublime: bar foo; foo bar", + "# st: baz foob", + "# st: fibz zap; zup blah" + ] + + actual = [ + "foo bar", + "bar foo", + "foo bar", + "baz foob", + "fibz zap", + "zup blah", + ] + + assert actual == [x for x in sublime_modelines.gen_raw_options(mdls)] + + +def test_gen_modeline_options(view): + set = view.settings().set + + gen_modelines = mock.Mock() + gen_modelines.return_value = ["# sublime: foo bar", + "# sublime: baz zoom"] + + gen_raw_options = mock.Mock() + gen_raw_options.return_value = ["foo bar", + "baz zoom"] + + sublime_modelines.gen_modelines = gen_modelines + sublime_modelines.gen_raw_options = gen_raw_options + + actual = [x for x in sublime_modelines.gen_modeline_options(view)] + assert [(set, "foo", "bar"), (set, "baz", "zoom")] == actual + + +def test_is_modeline(view): + sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") + view.substr.return_value = "# sublime: " + assert sublime_modelines.is_modeline(view, 0) + + +def test_to_json_type(): + a = "1" + b = "1.0" + c = "false" + d = "true" + e = list() + + assert sublime_modelines.to_json_type(a) == 1 + assert sublime_modelines.to_json_type(b) == 1.0 + assert sublime_modelines.to_json_type(c) == False + assert sublime_modelines.to_json_type(d) == True assert sublime_modelines.to_json_type(e) == e \ No newline at end of file From 540276e67c47b883bb0c4f3890d28c1087e89910 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 01:00:15 +0100 Subject: [PATCH 018/128] Dummy comments formatting fixes --- sublime_modelines.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 0c49c7d..e9741e0 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -178,8 +178,7 @@ def gen_modelines(view): candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) # Consider modelines at the end of the buffer too. - # There might be overlap with the top region, but it doesn't matter because - # it means the buffer is tiny. + # There might be overlap with the top region, but it doesn’t matter because it means the buffer is tiny. bottomRegStart = filter(lambda x: x > -1, ((view.size() - MODELINES_REG_SIZE), 0)) @@ -323,8 +322,7 @@ def to_json_type(v): class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): """This plugin provides a feature similar to vim modelines. - Modelines set options local to the view by declaring them in the - source code file itself. + Modelines set options local to the view by declaring them in the source code file itself. Example: mysourcecodefile.py @@ -332,8 +330,7 @@ class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): # sublime: translate_tab_to_spaces true The top as well as the bottom of the buffer is scanned for modelines. - MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be - scanned. + MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be scanned. """ def do_modelines(self, view): settings = view.settings() From 58c110ec5b07bde5be2f4d46ffc39fb3d19b9a6c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 01:04:03 +0100 Subject: [PATCH 019/128] Update manifest file --- MANIFEST.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index e87ca7a..c8599e8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include sublime_modelines.py -include LICENSE.TXT -include README.rst -prune setup.py \ No newline at end of file +include License.txt +include Readme.md +prune setup.py From dd26106125ce48398cb9e0e0a1dfc3651f10766b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 01:34:56 +0100 Subject: [PATCH 020/128] Remove modeline from script --- sublime_modelines.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index e9741e0..947f728 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -1,5 +1,3 @@ -# vim:et:ai:ts=4:syn=python: - import sublime, sublime_plugin import re, sys, json, os From 396c317676e8b6a121310fc38062d07a4a7a6c4a Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 01:40:53 +0100 Subject: [PATCH 021/128] Remove unused files --- MANIFEST.in | 4 - setup.py | 583 ---------------------------------------------------- 2 files changed, 587 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c8599e8..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include sublime_modelines.py -include License.txt -include Readme.md -prune setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 1180c02..0000000 --- a/setup.py +++ /dev/null @@ -1,583 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Commands to build and manage .sublime-package archives with distutils.""" - -import os - -from distutils.core import Command -from distutils.filelist import FileList -from distutils.text_file import TextFile -from distutils import dir_util, dep_util, file_util, archive_util -from distutils import log -from distutils.core import setup -from distutils.errors import * - - -import os, string -import sys -from types import * -from glob import glob -from distutils.core import Command -from distutils import dir_util, dep_util, file_util, archive_util -from distutils.text_file import TextFile -from distutils.errors import * -from distutils.filelist import FileList -from distutils import log - -import os -from distutils.errors import DistutilsExecError -from distutils.spawn import spawn -from distutils.dir_util import mkpath -from distutils import log - -def make_zipfile (base_name, base_dir, verbose=0, dry_run=0): - """Create a zip file from all the files under 'base_dir'. The output - zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" - Python module (if available) or the InfoZIP "zip" utility (if installed - and found on the default search path). If neither tool is available, - raises DistutilsExecError. Returns the name of the output zip file. - """ - try: - import zipfile - except ImportError: - zipfile = None - - zip_filename = base_name + ".sublime-package" - mkpath(os.path.dirname(zip_filename), dry_run=dry_run) - - # If zipfile module is not available, try spawning an external - # 'zip' command. - if zipfile is None: - if verbose: - zipoptions = "-r" - else: - zipoptions = "-rq" - - try: - spawn(["zip", zipoptions, zip_filename, base_dir], - dry_run=dry_run) - except DistutilsExecError: - # XXX really should distinguish between "couldn't find - # external 'zip' command" and "zip failed". - raise DistutilsExecError( - ("unable to create zip file '%s': " - "could neither import the 'zipfile' module nor " - "find a standalone zip utility") % zip_filename) - - else: - log.info("creating '%s' and adding '%s' to it", - zip_filename, base_dir) - - if not dry_run: - z = zipfile.ZipFile(zip_filename, "w", - compression=zipfile.ZIP_DEFLATED) - - for dirpath, dirnames, filenames in os.walk(base_dir): - for name in filenames: - path = os.path.normpath(os.path.join(dirpath, name)) - if dirpath == base_dir: - arcname = name - else: - arcname = path - if os.path.isfile(path): - z.write(path, arcname) - log.info("adding '%s'" % path) - z.close() - - return zip_filename - - -def show_formats (): - """Print all possible values for the 'formats' option (used by - the "--help-formats" command-line option). - """ - from distutils.fancy_getopt import FancyGetopt - from distutils.archive_util import ARCHIVE_FORMATS - formats=[] - for format in ARCHIVE_FORMATS.keys(): - formats.append(("formats=" + format, None, - ARCHIVE_FORMATS[format][2])) - formats.sort() - pretty_printer = FancyGetopt(formats) - pretty_printer.print_help( - "List of available source distribution formats:") - -class spa (Command): - - description = "create a source distribution (tarball, zip file, etc.)" - - user_options = [ - ('template=', 't', - "name of manifest template file [default: MANIFEST.in]"), - ('manifest=', 'm', - "name of manifest file [default: MANIFEST]"), - ('use-defaults', None, - "include the default file set in the manifest " - "[default; disable with --no-defaults]"), - ('no-defaults', None, - "don't include the default file set"), - ('prune', None, - "specifically exclude files/directories that should not be " - "distributed (build tree, RCS/CVS dirs, etc.) " - "[default; disable with --no-prune]"), - ('no-prune', None, - "don't automatically exclude anything"), - ('manifest-only', 'o', - "just regenerate the manifest and then stop " - "(implies --force-manifest)"), - ('force-manifest', 'f', - "forcibly regenerate the manifest and carry on as usual"), - ('formats=', None, - "formats for source distribution (comma-separated list)"), - ('keep-temp', 'k', - "keep the distribution tree around after creating " + - "archive file(s)"), - ('dist-dir=', 'd', - "directory to put the source distribution archive(s) in " - "[default: dist]"), - ] - - boolean_options = ['use-defaults', 'prune', - 'manifest-only', 'force-manifest', - 'keep-temp'] - - help_options = [ - ('help-formats', None, - "list available distribution formats", show_formats), - ] - - negative_opt = {'no-defaults': 'use-defaults', - 'no-prune': 'prune' } - - default_format = { 'posix': 'gztar', - 'nt': 'zip' } - - def initialize_options (self): - # 'template' and 'manifest' are, respectively, the names of - # the manifest template and manifest file. - self.template = None - self.manifest = None - - # 'use_defaults': if true, we will include the default file set - # in the manifest - self.use_defaults = 1 - self.prune = 1 - - self.manifest_only = 0 - self.force_manifest = 0 - - self.formats = None - self.keep_temp = 0 - self.dist_dir = None - - self.archive_files = None - - - def finalize_options (self): - if self.manifest is None: - self.manifest = "MANIFEST" - if self.template is None: - self.template = "MANIFEST.in" - - self.ensure_string_list('formats') - if self.formats is None: - try: - self.formats = [self.default_format[os.name]] - except KeyError: - raise DistutilsPlatformError( - "don't know how to create source distributions " + \ - "on platform %s" % os.name) - - bad_format = archive_util.check_archive_formats(self.formats) - if bad_format: - raise DistutilsOptionError( - "unknown archive format '%s'" % bad_format) - - if self.dist_dir is None: - self.dist_dir = "dist" - - - def run (self): - - # 'filelist' contains the list of files that will make up the - # manifest - self.filelist = FileList() - - # Ensure that all required meta-data is given; warn if not (but - # don't die, it's not *that* serious!) - self.check_metadata() - - # Do whatever it takes to get the list of files to process - # (process the manifest template, read an existing manifest, - # whatever). File list is accumulated in 'self.filelist'. - self.get_file_list() - - # If user just wanted us to regenerate the manifest, stop now. - if self.manifest_only: - return - - # Otherwise, go ahead and create the source distribution tarball, - # or zipfile, or whatever. - self.make_distribution() - - - def check_metadata (self): - """Ensure that all required elements of meta-data (name, version, - URL, (author and author_email) or (maintainer and - maintainer_email)) are supplied by the Distribution object; warn if - any are missing. - """ - metadata = self.distribution.metadata - - missing = [] - for attr in ('name', 'version', 'url'): - if not (hasattr(metadata, attr) and getattr(metadata, attr)): - missing.append(attr) - - if missing: - self.warn("missing required meta-data: " + - string.join(missing, ", ")) - - if metadata.author: - if not metadata.author_email: - self.warn("missing meta-data: if 'author' supplied, " + - "'author_email' must be supplied too") - elif metadata.maintainer: - if not metadata.maintainer_email: - self.warn("missing meta-data: if 'maintainer' supplied, " + - "'maintainer_email' must be supplied too") - else: - self.warn("missing meta-data: either (author and author_email) " + - "or (maintainer and maintainer_email) " + - "must be supplied") - - # check_metadata () - - - def get_file_list (self): - """Figure out the list of files to include in the source - distribution, and put it in 'self.filelist'. This might involve - reading the manifest template (and writing the manifest), or just - reading the manifest, or just using the default file set -- it all - depends on the user's options and the state of the filesystem. - """ - - # If we have a manifest template, see if it's newer than the - # manifest; if so, we'll regenerate the manifest. - template_exists = os.path.isfile(self.template) - if template_exists: - template_newer = dep_util.newer(self.template, self.manifest) - - # The contents of the manifest file almost certainly depend on the - # setup script as well as the manifest template -- so if the setup - # script is newer than the manifest, we'll regenerate the manifest - # from the template. (Well, not quite: if we already have a - # manifest, but there's no template -- which will happen if the - # developer elects to generate a manifest some other way -- then we - # can't regenerate the manifest, so we don't.) - self.debug_print("checking if %s newer than %s" % - (self.distribution.script_name, self.manifest)) - setup_newer = dep_util.newer(self.distribution.script_name, - self.manifest) - - # cases: - # 1) no manifest, template exists: generate manifest - # (covered by 2a: no manifest == template newer) - # 2) manifest & template exist: - # 2a) template or setup script newer than manifest: - # regenerate manifest - # 2b) manifest newer than both: - # do nothing (unless --force or --manifest-only) - # 3) manifest exists, no template: - # do nothing (unless --force or --manifest-only) - # 4) no manifest, no template: generate w/ warning ("defaults only") - - manifest_outofdate = (template_exists and - (template_newer or setup_newer)) - force_regen = self.force_manifest or self.manifest_only - manifest_exists = os.path.isfile(self.manifest) - neither_exists = (not template_exists and not manifest_exists) - - # Regenerate the manifest if necessary (or if explicitly told to) - if manifest_outofdate or neither_exists or force_regen: - if not template_exists: - self.warn(("manifest template '%s' does not exist " + - "(using default file list)") % - self.template) - self.filelist.findall() - - if self.use_defaults: - self.add_defaults() - if template_exists: - self.read_template() - if self.prune: - self.prune_file_list() - - self.filelist.sort() - self.filelist.remove_duplicates() - self.write_manifest() - - # Don't regenerate the manifest, just read it in. - else: - self.read_manifest() - - # get_file_list () - - - def add_defaults (self): - """Add all the default files to self.filelist: - - README or README.txt - - setup.py - - test/test*.py - - all pure Python modules mentioned in setup script - - all C sources listed as part of extensions or C libraries - in the setup script (doesn't catch C headers!) - Warns if (README or README.txt) or setup.py are missing; everything - else is optional. - """ - - standards = [('README', 'README.txt'), self.distribution.script_name] - for fn in standards: - # XXX - if fn == 'setup.py': continue # We don't want setup.py - if type(fn) is TupleType: - alts = fn - got_it = 0 - for fn in alts: - if os.path.exists(fn): - got_it = 1 - self.filelist.append(fn) - break - - if not got_it: - self.warn("standard file not found: should have one of " + - string.join(alts, ', ')) - else: - if os.path.exists(fn): - self.filelist.append(fn) - else: - self.warn("standard file '%s' not found" % fn) - - optional = ['test/test*.py', 'setup.cfg'] - for pattern in optional: - files = filter(os.path.isfile, glob(pattern)) - if files: - self.filelist.extend(files) - - if self.distribution.has_pure_modules(): - build_py = self.get_finalized_command('build_py') - self.filelist.extend(build_py.get_source_files()) - - if self.distribution.has_ext_modules(): - build_ext = self.get_finalized_command('build_ext') - self.filelist.extend(build_ext.get_source_files()) - - if self.distribution.has_c_libraries(): - build_clib = self.get_finalized_command('build_clib') - self.filelist.extend(build_clib.get_source_files()) - - if self.distribution.has_scripts(): - build_scripts = self.get_finalized_command('build_scripts') - self.filelist.extend(build_scripts.get_source_files()) - - # add_defaults () - - - def read_template (self): - """Read and parse manifest template file named by self.template. - - (usually "MANIFEST.in") The parsing and processing is done by - 'self.filelist', which updates itself accordingly. - """ - log.info("reading manifest template '%s'", self.template) - template = TextFile(self.template, - strip_comments=1, - skip_blanks=1, - join_lines=1, - lstrip_ws=1, - rstrip_ws=1, - collapse_join=1) - - while 1: - line = template.readline() - if line is None: # end of file - break - - try: - self.filelist.process_template_line(line) - except DistutilsTemplateError(msg): - self.warn("%s, line %d: %s" % (template.filename, - template.current_line, - msg)) - - # read_template () - - - def prune_file_list (self): - """Prune off branches that might slip into the file list as created - by 'read_template()', but really don't belong there: - * the build tree (typically "build") - * the release tree itself (only an issue if we ran "spa" - previously with --keep-temp, or it aborted) - * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories - """ - build = self.get_finalized_command('build') - base_dir = self.distribution.get_fullname() - - self.filelist.exclude_pattern(None, prefix=build.build_base) - self.filelist.exclude_pattern(None, prefix=base_dir) - - # pruning out vcs directories - # both separators are used under win32 - if sys.platform == 'win32': - seps = r'/|\\' - else: - seps = '/' - - vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', - '_darcs'] - vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps) - self.filelist.exclude_pattern(vcs_ptrn, is_regex=1) - - def write_manifest (self): - """Write the file list in 'self.filelist' (presumably as filled in - by 'add_defaults()' and 'read_template()') to the manifest file - named by 'self.manifest'. - """ - self.execute(file_util.write_file, - (self.manifest, self.filelist.files), - "writing manifest file '%s'" % self.manifest) - - # write_manifest () - - - def read_manifest (self): - """Read the manifest file (named by 'self.manifest') and use it to - fill in 'self.filelist', the list of files to include in the source - distribution. - """ - log.info("reading manifest file '%s'", self.manifest) - manifest = open(self.manifest) - while 1: - line = manifest.readline() - if line == '': # end of file - break - if line[-1] == '\n': - line = line[0:-1] - self.filelist.append(line) - manifest.close() - - # read_manifest () - - - def make_release_tree (self, base_dir, files): - """Create the directory tree that will become the source - distribution archive. All directories implied by the filenames in - 'files' are created under 'base_dir', and then we hard link or copy - (if hard linking is unavailable) those files into place. - Essentially, this duplicates the developer's source tree, but in a - directory named after the distribution, containing only the files - to be distributed. - """ - # Create all the directories under 'base_dir' necessary to - # put 'files' there; the 'mkpath()' is just so we don't die - # if the manifest happens to be empty. - self.mkpath(base_dir) - dir_util.create_tree(base_dir, files, dry_run=self.dry_run) - - # And walk over the list of files, either making a hard link (if - # os.link exists) to each one that doesn't already exist in its - # corresponding location under 'base_dir', or copying each file - # that's out-of-date in 'base_dir'. (Usually, all files will be - # out-of-date, because by default we blow away 'base_dir' when - # we're done making the distribution archives.) - - if hasattr(os, 'link'): # can make hard links on this system - link = 'hard' - msg = "making hard links in %s..." % base_dir - else: # nope, have to copy - link = None - msg = "copying files to %s..." % base_dir - - if not files: - log.warn("no files to distribute -- empty manifest?") - else: - log.info(msg) - for file in files: - if not os.path.isfile(file): - log.warn("'%s' not a regular file -- skipping" % file) - else: - dest = os.path.join(base_dir, file) - self.copy_file(file, dest, link=link) - - self.distribution.metadata.write_pkg_info(base_dir) - - # make_release_tree () - - def make_distribution (self): - """Create the source distribution(s). First, we create the release - tree with 'make_release_tree()'; then, we create all required - archive files (according to 'self.formats') from the release tree. - Finally, we clean up by blowing away the release tree (unless - 'self.keep_temp' is true). The list of archive files created is - stored so it can be retrieved later by 'get_archive_files()'. - """ - # Don't warn about missing meta-data here -- should be (and is!) - # done elsewhere. - # base_dir = self.distribution.get_fullname() - base_dir = self.distribution.get_name() - # XXX - base_dir = base_dir - base_name = os.path.join(self.dist_dir, base_dir) - - - self.make_release_tree(base_dir, self.filelist.files) - archive_files = [] # remember names of files we create - # tar archive must be created last to avoid overwrite and remove - if 'tar' in self.formats: - self.formats.append(self.formats.pop(self.formats.index('tar'))) - - for fmt in self.formats: - # file = self.make_archive(base_name, fmt, base_dir=base_dir) - file = make_zipfile(base_name, base_dir=base_dir) - archive_files.append(file) - self.distribution.dist_files.append(('spa', '', file)) - - self.archive_files = archive_files - - if not self.keep_temp: - dir_util.remove_tree(base_dir, dry_run=self.dry_run) - - def get_archive_files (self): - """Return the list of archive files created when the command - was run, or None if the command hasn't run yet. - """ - return self.archive_files - -# class spa - - -class install(Command): - """Does it make sense?""" - - user_options = [('aa', 'a', 'aa')] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - print(NotImplementedError("Command not implemented yet.")) - - -setup(cmdclass={'spa': spa, 'install': install}, - name='SublimeModelines', - version='1.1', - description='Vim-like modelines for Sublime Text.', - author='Guillermo López-Anglada', - author_email='guillermo@sublimetext.info', - url='http://sublimetext.info', - py_modules=['sublime_modelines.py'] - ) \ No newline at end of file From 4d8e7ac1835b89f54a88d43dfc2fab62f0e60008 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 02:45:00 +0100 Subject: [PATCH 022/128] Use double-quotes instead of simple-quotes --- sublime_modelines.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 947f728..5950f71 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -16,15 +16,15 @@ KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") -DEFAULT_LINE_COMMENT = '#' -MULTIOPT_SEP = '; ' +DEFAULT_LINE_COMMENT = "#" +MULTIOPT_SEP = "; " MAX_LINES_TO_CHECK = 50 LINE_LENGTH = 80 MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH -MONITORED_OUTPUT_PANELS = ['exec'] +MONITORED_OUTPUT_PANELS = ["exec"] -ST3 = sublime.version() >= '3000' +ST3 = sublime.version() >= "3000" if ST3: basestring = str @@ -193,7 +193,7 @@ def gen_modelines(view): yield modeline def vim_mapped(t, s): - if t == 'vim' or len(s) < 3: + if t == "vim" or len(s) < 3: while s in VIM_MAP: s = VIM_MAP[s] return s[0] @@ -212,7 +212,7 @@ def gen_raw_options(modelines): type, s = match.groups() while True: - if s.startswith(':'): s = s[1:] + if s.startswith(":"): s = s[1:] m = KEY_VALUE.match(s) if m: @@ -227,13 +227,13 @@ def gen_raw_options(modelines): value = "true" _k = vim_mapped(type, k) - if (k.startswith('no') and (type == 'vim' or ( + if (k.startswith("no") and (type == "vim" or ( k[2:] in VIM_MAP or len(k) <= 4))): value = "false" _k = vim_mapped(type, k[2:]) - yield _k, '=', value + yield _k, "=", value s = s[m.end():] continue @@ -243,7 +243,7 @@ def gen_raw_options(modelines): continue # original sublime modelines style - opt = m.partition(':')[2].strip() + opt = m.partition(":")[2].strip() if MULTIOPT_SEP in opt: for subopt in (s for s in opt.split(MULTIOPT_SEP)): yield subopt @@ -256,17 +256,17 @@ def gen_modeline_options(view): for opt in gen_raw_options(modelines): if not isinstance(opt, tuple): #import spdb ; spdb.start() - name, sep, value = opt.partition(' ') - yield view.settings().set, name.rstrip(':'), value.rstrip(';') + name, sep, value = opt.partition(" ") + yield view.settings().set, name.rstrip(":"), value.rstrip(";") else: name, op, value = opt def _setter(n,v): - if op == '+=': - if v.startswith('{'): + if op == "+=": + if v.startswith("{"): default = {} - elif v.startswith('['): + elif v.startswith("["): default = [] elif isinstance(v, basestring): default = "" @@ -333,19 +333,19 @@ class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): def do_modelines(self, view): settings = view.settings() - ignored_packages = settings.get('ignored_packages') + ignored_packages = settings.get("ignored_packages") - keys = set(settings.get('sublime_modelines_keys', [])) + keys = set(settings.get("sublime_modelines_keys", [])) new_keys = set() - base_dir = settings.get('result_base_dir') + base_dir = settings.get("result_base_dir") for setter, name, value in gen_modeline_options(view): - #if 'vim' in MODELINE_PREFIX_TPL: # vimsupport + #if "vim" in MODELINE_PREFIX_TPL: # vimsupport # vim_map.get(name) debug_log("modeline: %s = %s", name, value) - if name in ('x_syntax', 'syntax'): + if name in ("x_syntax", "syntax"): syntax_file = None if os.path.isabs(value): @@ -389,7 +389,7 @@ def do_modelines(self, view): else: view.set_syntax_file(syntax_file) - new_keys.add('syntax') + new_keys.add("syntax") debug_log("set syntax = %s" % syntax_file) else: @@ -398,7 +398,7 @@ def do_modelines(self, view): new_keys.add(name) except ValueError as e: sublime.status_message("[SublimeModelines] Bad modeline detected.") - console_log("Bad option detected: %s, %s", name, value) + console_log("Bad option detected: %s, %s.", name, value) console_log("Tip: Keys cannot be empty strings.") for k in keys: From 13cfdf983dba465a97e5923fa8c57d126559ad38 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 02:47:52 +0100 Subject: [PATCH 023/128] Ident empty lines --- sublime_modelines.py | 50 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 5950f71..0328e36 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -207,41 +207,41 @@ def gen_raw_options(modelines): match = MODELINE_TYPE_1.search(m) if not match: match = MODELINE_TYPE_2.search(m) - + if match: type, s = match.groups() - + while True: if s.startswith(":"): s = s[1:] - + m = KEY_VALUE.match(s) if m: key, op, value = m.groups() yield vim_mapped(type, key), op, value s = s[m.end():] continue - + m = KEY_ONLY.match(s) if m: k, = m.groups() value = "true" - + _k = vim_mapped(type, k) if (k.startswith("no") and (type == "vim" or ( k[2:] in VIM_MAP or len(k) <= 4))): - + value = "false" _k = vim_mapped(type, k[2:]) - + yield _k, "=", value - + s = s[m.end():] continue - + break - + continue - + # original sublime modelines style opt = m.partition(":")[2].strip() if MULTIOPT_SEP in opt: @@ -258,10 +258,10 @@ def gen_modeline_options(view): #import spdb ; spdb.start() name, sep, value = opt.partition(" ") yield view.settings().set, name.rstrip(":"), value.rstrip(";") - + else: name, op, value = opt - + def _setter(n,v): if op == "+=": if v.startswith("{"): @@ -272,12 +272,12 @@ def _setter(n,v): default = "" else: default = 0 - + ov = view.settings().get(n, default) v = ov + v - + view.settings().set(n,v) - + yield _setter, name, value @@ -294,7 +294,7 @@ def get_line_comment_char(view): break except TypeError: pass - + if not commentChar2: return re.escape(commentChar.strip()) else: @@ -321,25 +321,25 @@ def to_json_type(v): class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): """This plugin provides a feature similar to vim modelines. Modelines set options local to the view by declaring them in the source code file itself. - + Example: mysourcecodefile.py # sublime: gutter false # sublime: translate_tab_to_spaces true - + The top as well as the bottom of the buffer is scanned for modelines. MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be scanned. """ def do_modelines(self, view): settings = view.settings() - + ignored_packages = settings.get("ignored_packages") - + keys = set(settings.get("sublime_modelines_keys", [])) new_keys = set() - + base_dir = settings.get("result_base_dir") - + for setter, name, value in gen_modeline_options(view): #if "vim" in MODELINE_PREFIX_TPL: # vimsupport # vim_map.get(name) @@ -391,7 +391,7 @@ def do_modelines(self, view): new_keys.add("syntax") debug_log("set syntax = %s" % syntax_file) - + else: try: setter(name, to_json_type(value)) @@ -400,7 +400,7 @@ def do_modelines(self, view): sublime.status_message("[SublimeModelines] Bad modeline detected.") console_log("Bad option detected: %s, %s.", name, value) console_log("Tip: Keys cannot be empty strings.") - + for k in keys: if k not in new_keys: if settings.has(k): From 195d48af8a8ca059660a5bf3af963fd19a8c609d Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 02:49:03 +0100 Subject: [PATCH 024/128] Dummy syntax changes --- sublime_modelines.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 0328e36..cb4cdf7 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -162,15 +162,12 @@ def get_language_files(ignored_packages, *paths): return result def get_output_panel(name): - if ST3: - return sublime.active_window().create_output_panel(name) - else: - return sublime.active_window().get_output_panel(name) + if ST3: return sublime.active_window().create_output_panel(name) + else: return sublime.active_window().get_output_panel(name) def is_modeline(prefix, line): return bool(re.match(prefix, line)) - def gen_modelines(view): topRegEnd = min(MODELINES_REG_SIZE, view.size()) candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) @@ -242,7 +239,7 @@ def gen_raw_options(modelines): continue - # original sublime modelines style + # Original sublime modelines style. opt = m.partition(":")[2].strip() if MULTIOPT_SEP in opt: for subopt in (s for s in opt.split(MULTIOPT_SEP)): From 3c60255800dd9334e1a3a5f172b51261b625e591 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 02:51:01 +0100 Subject: [PATCH 025/128] Fix and make less flexible syntax setting --- sublime_modelines.py | 190 +++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 98 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index cb4cdf7..25da1d1 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -1,6 +1,23 @@ import sublime, sublime_plugin import re, sys, json, os + +def log_to_file(str): + with open("/tmp/modelines_debug.log", "a") as myfile: + myfile.write(str + "\n") + +def log_to_console(s, *args): + log_to_file("[SublimeModelines] "+(s % args)) + sys.stderr.write("[SublimeModelines] " + (s % args) + "\n") + +def debug_log(s, *args): + if True: + log_to_console(s, *args) + + +debug_log("Modelines plugin start.") + + MODELINE_PREFIX_TPL = "%s\\s*(st|sublime|vim):" MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime|vim):\x20?set\x20(.*):.*$") @@ -129,38 +146,6 @@ # "wrap_width": 0, } -def console_log(s, *args): - sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") - -def debug_log(s, *args): - if 0: - sys.stderr.write('[SublimeModelines] '+(s % args)+"\n") - -def get_language_files(ignored_packages, *paths): - paths = list(paths) - tml_files = [] - if ST3: - tml_files.extend(sublime.find_resources('*.tmLanguage')) - else: - paths.insert(0, sublime.packages_path()) - - for path in paths: - for dir, dirs, files in os.walk(path): - # TODO: be sure that not tmLanguage from disabled package is taken - for fn in files: - if fn.endswith('.tmLanguage'): - tml_files.append(os.path.join(dir, fn)) - - R = re.compile("Packages[\\/]([^\\/]+)[\\/]") - result = [] - for f in tml_files: - m = R.search(f) - if m: - if m.group(1) not in ignored_packages: - result.append(f) - - return result - def get_output_panel(name): if ST3: return sublime.active_window().create_output_panel(name) else: return sublime.active_window().get_output_panel(name) @@ -327,7 +312,34 @@ class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): The top as well as the bottom of the buffer is scanned for modelines. MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be scanned. """ + + settings = None + + def __init__(self): + self._modes = {} + + def on_load(self, view): + debug_log("on_load") + self.do_modelines(view) + + def on_post_save(self, view): + debug_log("on_post_save") + self.do_modelines(view) + + if 0: + def on_modified(self, view): + for p in MONITORED_OUTPUT_PANELS: + v = get_output_panel(p) + if v.id() != view.id(): continue + return + + self.do_modelines(view) + return + def do_modelines(self, view): + if not self._modes: + self.init_syntax_files() + settings = view.settings() ignored_packages = settings.get("ignored_packages") @@ -341,52 +353,16 @@ def do_modelines(self, view): #if "vim" in MODELINE_PREFIX_TPL: # vimsupport # vim_map.get(name) debug_log("modeline: %s = %s", name, value) - - if name in ("x_syntax", "syntax"): + + if name == "x_syntax": syntax_file = None - - if os.path.isabs(value): - syntax_file = value - - if not os.path.exists(syntax_file): - console_log("%s does not exist", value) - continue - - else: - # be smart about syntax: - if base_dir: - lang_files = get_language_files(ignored_packages, base_dir) - else: - lang_files = get_language_files(ignored_packages) - - #lang_files.sort(key=lambda x: len(os.path.basename(x))) - - candidates = [] - for syntax_file in lang_files: - if value in os.path.basename(syntax_file): - candidates.append(syntax_file) - - value_lower = value.lower() - if not candidates: - for syntax_file in lang_files: - if value_lower in os.path.basename(syntax_file).lower(): - candidates.append(syntax_file) - - if not candidates: - console_log("%s cannot be resolved to a syntaxfile", value) - syntax_file = None - continue - - else: - candidates.sort(key=lambda x: len(os.path.basename(x))) - syntax_file = candidates[0] - - if ST3: - view.assign_syntax(syntax_file) - else: - view.set_syntax_file(syntax_file) - - new_keys.add("syntax") + if value.lower() in self._modes: syntax_file = self._modes[value.lower()] + else: syntax_file = value + + if ST3: view.assign_syntax(syntax_file) + else: view.set_syntax_file(syntax_file) + + new_keys.add("x_syntax") debug_log("set syntax = %s" % syntax_file) else: @@ -395,29 +371,47 @@ def do_modelines(self, view): new_keys.add(name) except ValueError as e: sublime.status_message("[SublimeModelines] Bad modeline detected.") - console_log("Bad option detected: %s, %s.", name, value) - console_log("Tip: Keys cannot be empty strings.") + log_to_console("Bad option detected: %s, %s.", name, value) + log_to_console("Tip: Keys cannot be empty strings.") for k in keys: if k not in new_keys: if settings.has(k): settings.erase(k) - - settings.set('sublime_modelines_keys', list(new_keys)) - - - def on_load(self, view): - self.do_modelines(view) - - def on_post_save(self, view): - self.do_modelines(view) - - if 0: - def on_modified(self, view): - for p in MONITORED_OUTPUT_PANELS: - v = get_output_panel(p) - if v.id() != view.id(): continue - return - - self.do_modelines(view) - return + + settings.set("sublime_modelines_keys", list(new_keys)) + + + # From . + def init_syntax_files(self): + for syntax_file in self.find_syntax_files(): + name = os.path.splitext(os.path.basename(syntax_file))[0].lower() + self._modes[name] = syntax_file + + # Load custom mappings from the settings file. + self.settings = sublime.load_settings("SublimeModelines.sublime-settings") + + if self.settings.has("mode_mappings"): + for modeline, syntax in self.settings.get("mode_mappings").items(): + self._modes[modeline] = self._modes[syntax.lower()] + + if self.settings.has("user_mode_mappings"): + for modeline, syntax in self.settings.get("user_mode_mappings").items(): + self._modes[modeline] = self._modes[syntax.lower()] + + + # From . + def find_syntax_files(self): + # ST3 + if hasattr(sublime, "find_resources"): + for f in sublime.find_resources("*.tmLanguage"): + yield f + for f in sublime.find_resources("*.sublime-syntax"): + yield f + else: + for root, dirs, files in os.walk(sublime.packages_path()): + for f in files: + if f.endswith(".tmLanguage") or f.endswith("*.sublime-syntax"): + langfile = os.path.relpath(os.path.join(root, f), sublime.packages_path()) + # ST2 (as of build 2181) requires unix/MSYS style paths for the “syntax” view setting. + yield os.path.join("Packages", langfile).replace("\\", "/") From 5ee3bfed8f4fbb201ef6cead30d86b55f9ac4028 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 02:55:20 +0100 Subject: [PATCH 026/128] Dummy code formatting change --- Modelines.sublime-commands | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Modelines.sublime-commands b/Modelines.sublime-commands index c205086..939be46 100644 --- a/Modelines.sublime-commands +++ b/Modelines.sublime-commands @@ -1,5 +1,7 @@ [ - { "caption": "Modelines: Run Tests", - "command": "run_plugin_unittest", - "args": {"module": "Modelines.tests"} } -] \ No newline at end of file + { + "caption": "Modelines: Run Tests", + "command": "run_plugin_unittest", + "args": {"module": "Modelines.tests"}, + } +] From 3cb77eb0fc68523329e7dc3b07b02c07fc2cb5ca Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:16:26 +0100 Subject: [PATCH 027/128] Remove some dead code --- sublime_modelines.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 25da1d1..864c003 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -39,8 +39,6 @@ def debug_log(s, *args): LINE_LENGTH = 80 MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH -MONITORED_OUTPUT_PANELS = ["exec"] - ST3 = sublime.version() >= "3000" if ST3: @@ -326,16 +324,6 @@ def on_post_save(self, view): debug_log("on_post_save") self.do_modelines(view) - if 0: - def on_modified(self, view): - for p in MONITORED_OUTPUT_PANELS: - v = get_output_panel(p) - if v.id() != view.id(): continue - return - - self.do_modelines(view) - return - def do_modelines(self, view): if not self._modes: self.init_syntax_files() From d33197266d6388d3215de59d912be81990450b2e Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:16:50 +0100 Subject: [PATCH 028/128] Remove vim support --- sublime_modelines.py | 128 ++----------------------------------------- 1 file changed, 5 insertions(+), 123 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 864c003..eebdfb7 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -18,10 +18,10 @@ def debug_log(s, *args): debug_log("Modelines plugin start.") -MODELINE_PREFIX_TPL = "%s\\s*(st|sublime|vim):" +MODELINE_PREFIX_TPL = "%s\\s*(st|sublime):" -MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime|vim):\x20?set\x20(.*):.*$") -MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime|vim):(.*):.*$") +MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime):\x20?set\x20(.*):.*$") +MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime):(.*):.*$") KEY_VALUE = re.compile(r"""(?x) \s* (?P\w+) \s* (?P\+?=) \s* (?P @@ -44,106 +44,6 @@ def debug_log(s, *args): if ST3: basestring = str -VIM_MAP = { - #"gfn": "guifont" - #"guifont": {"regex": ..., 1: "font_face", 2: ("font_size", int)} - - "ts": "tabstop", - "tabstop": ("tab_size", int), - "ai": "autoindent", - "autoindent": ("auto_indent", bool), - "et": "expandtab", - "expandtab": ("translate_tabs_to_spaces", bool), - "syn": "syntax", - "syntax": ("syntax", str), - "nu": "number", - "number": ("line_numbers", bool), - - # "always_show_minimap_viewport": false, - # "animation_enabled": true, - # "atomic_save": true, - # "auto_close_tags": true, - # "auto_complete": true, - # "auto_complete_commit_on_tab": false, - # "auto_complete_delay": 50, - # "auto_complete_selector": "source - comment, meta.tag - punctuation.definition.tag.begin", - # "auto_complete_size_limit": 4194304, - # "auto_complete_triggers": [ {"selector": "text.html", "characters": "<"} ], - # "auto_complete_with_fields": false, - # "auto_find_in_selection": false, - # "auto_indent": true, - # "auto_match_enabled": true, - # "binary_file_patterns": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.ttf", "*.tga", "*.dds", "*.ico", "*.eot", "*.pdf", "*.swf", "*.jar", "*.zip"], - # "bold_folder_labels": false, - # "caret_style": "smooth", - # "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", - # "copy_with_empty_selection": true, - # "default_encoding": "UTF-8", - # "default_line_ending": "system", - # "detect_indentation": true, - # "dictionary": "Packages/Language - English/en_US.dic", - # "drag_text": true, - # "draw_centered": false, - # "draw_indent_guides": true, - # "draw_minimap_border": false, - # "draw_white_space": "selection", - # "enable_hexadecimal_encoding": true, - # "enable_telemetry": "auto", - # "ensure_newline_at_eof_on_save": false, - # "fade_fold_buttons": true, - # "fallback_encoding": "Western (Windows 1252)", - # "file_exclude_patterns": ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db", "*.sublime-workspace"], - # "find_selected_text": true, - # "fold_buttons": true, - # "folder_exclude_patterns": [".svn", ".git", ".hg", "CVS"], - # "font_face": "", - # "font_options": [], # list - # "font_size": 10, - # "gpu_window_buffer": "auto", - # "gutter": true, - # "highlight_line": false, - # "highlight_modified_tabs": false, - # "ignored_packages": ["Vintage"] - # "indent_guide_options": ["draw_normal"], - # "indent_subsequent_lines": true, - # "indent_to_bracket": false, - # "index_files": true, - # "line_padding_bottom": 0, - # "line_padding_top": 0, - # "margin": 4, - # "match_brackets": true, - # "match_brackets_angle": false, - # "match_brackets_braces": true, - # "match_brackets_content": true, - # "match_brackets_square": true, - # "match_selection": true, - # "match_tags": true, - # "move_to_limit_on_up_down": false, - # "overlay_scroll_bars": "system", - # "preview_on_click": true, - # "rulers": [], # list - # "save_on_focus_lost": false, - # "scroll_past_end": true, - # "scroll_speed": 1.0, - # "shift_tab_unindent": false, - # "show_panel_on_build": true, - # "show_tab_close_buttons": true, - # "smart_indent": true, - # "spell_check": false, - # "tab_completion": true, - # "tab_size": 4, - # "theme": "Default.sublime-theme", - # "translate_tabs_to_spaces": false, - # "tree_animation_enabled": true, - # "trim_automatic_white_space": true, - # "trim_trailing_white_space_on_save": false, - # "use_simple_full_screen": false, - # "use_tab_stops": true, - # "word_separators": "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?", - # "word_wrap": "auto", - # "wrap_width": 0, -} - def get_output_panel(name): if ST3: return sublime.active_window().create_output_panel(name) else: return sublime.active_window().get_output_panel(name) @@ -172,14 +72,6 @@ def gen_modelines(view): for modeline in modelines: yield modeline -def vim_mapped(t, s): - if t == "vim" or len(s) < 3: - while s in VIM_MAP: - s = VIM_MAP[s] - return s[0] - else: - return s - def gen_raw_options(modelines): #import spdb ; spdb.start() @@ -196,8 +88,7 @@ def gen_raw_options(modelines): m = KEY_VALUE.match(s) if m: - key, op, value = m.groups() - yield vim_mapped(type, key), op, value + yield m.groups() s = s[m.end():] continue @@ -206,14 +97,7 @@ def gen_raw_options(modelines): k, = m.groups() value = "true" - _k = vim_mapped(type, k) - if (k.startswith("no") and (type == "vim" or ( - k[2:] in VIM_MAP or len(k) <= 4))): - - value = "false" - _k = vim_mapped(type, k[2:]) - - yield _k, "=", value + yield k, "=", value s = s[m.end():] continue @@ -338,8 +222,6 @@ def do_modelines(self, view): base_dir = settings.get("result_base_dir") for setter, name, value in gen_modeline_options(view): - #if "vim" in MODELINE_PREFIX_TPL: # vimsupport - # vim_map.get(name) debug_log("modeline: %s = %s", name, value) if name == "x_syntax": From a8883372510906ee0beed6226b547696f5b443c0 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:17:11 +0100 Subject: [PATCH 029/128] Add info about setting language name for x_syntax in Readme --- Readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index e281cfe..0097118 100644 --- a/Readme.md +++ b/Readme.md @@ -66,7 +66,9 @@ For some common cases, no directly settable option exists (for example, a settin For such cases, Sublime Modelines provides non-standard accessors as a stop-gap solution. ```text -x_syntax Packages/Foo/Foo.tmLanguage +# sublime: x_syntax Foo +or +# sublime: x_syntax Packages/Foo/Foo.tmLanguage ``` Sets the syntax to the specified `.tmLanguage` file. From 0dea4eed555fb08f9fd1248ebe04c2f55738b8c5 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:18:33 +0100 Subject: [PATCH 030/128] Fix some rst to md conversion failures --- Readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 0097118..12a584d 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,7 @@ # Sublime Modelines Set settings local to a single buffer. -A more granular approach to settings than the per file type ``.sublime-settings`` files. +A more granular approach to settings than the per file type `.sublime-settings` files. Inspired by Vim’s modelines feature. @@ -39,7 +39,7 @@ Modelines must be declared at the top or the bottom of source code files with on ``` **Note**: -``#`` is the default comment character. +`#` is the default comment character. Use the corresponding single-line comment character for your language. When there isn't a concept of comment, the default comment character must be used. @@ -50,7 +50,7 @@ SublimeModelines finds the appropriate single-line comment character by inspecti To see an example of how this is done, open `Packages/Python/Miscellaneous.tmPreferences`. Many packages giving support for programming languages already include this, - but you might need to create a ``.tmPreferences`` file for the language you're working with + but you might need to create a `.tmPreferences` file for the language you're working with if you want SublimeModelines to be available. From 337c2fa253fa4517b0e0d9e3ca31b0718bc785a8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:26:53 +0100 Subject: [PATCH 031/128] Remove some dead code --- sublime_modelines.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index eebdfb7..527c5eb 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -57,14 +57,10 @@ def gen_modelines(view): # Consider modelines at the end of the buffer too. # There might be overlap with the top region, but it doesn’t matter because it means the buffer is tiny. - bottomRegStart = filter(lambda x: x > -1, - ((view.size() - MODELINES_REG_SIZE), 0)) - bottomRegStart = view.size() - MODELINES_REG_SIZE - if bottomRegStart < 0: bottomRegStart = 0 - candidates += view.lines( sublime.Region(bottomRegStart, view.size()) ) + candidates += view.lines(sublime.Region(bottomRegStart, view.size())) prefix = build_modeline_prefix(view) modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) From c4f6e22fc25493f985914c8528e093017210272c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:27:09 +0100 Subject: [PATCH 032/128] Disable debug logging --- sublime_modelines.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 527c5eb..b1750a9 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -2,16 +2,19 @@ import re, sys, json, os +enable_debug_log = False + def log_to_file(str): - with open("/tmp/modelines_debug.log", "a") as myfile: - myfile.write(str + "\n") + if enable_debug_log: + with open("/tmp/modelines_debug.log", "a") as myfile: + myfile.write(str + "\n") def log_to_console(s, *args): log_to_file("[SublimeModelines] "+(s % args)) sys.stderr.write("[SublimeModelines] " + (s % args) + "\n") def debug_log(s, *args): - if True: + if enable_debug_log: log_to_console(s, *args) From 241186f0144b4861a3b5b93d605516be37857a04 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:29:46 +0100 Subject: [PATCH 033/128] Import importlib instead of imp --- tests/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 72f1b11..a34d8ff 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -from imp import reload +from importlib import reload from . import test_modelines reload(test_modelines) @@ -7,4 +7,3 @@ reload(sublime_modelines) from .test_modelines import * - From 7c3b0c12fc65dae3df3eef8f417d3cf72d30db7b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 03:35:07 +0100 Subject: [PATCH 034/128] Differentiate logging to log and to tmp --- sublime_modelines.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index b1750a9..15126a2 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -3,9 +3,10 @@ enable_debug_log = False +enable_log_to_tmp = False def log_to_file(str): - if enable_debug_log: + if enable_log_to_tmp: with open("/tmp/modelines_debug.log", "a") as myfile: myfile.write(str + "\n") From bee07e614fa1c7d059d493181eb4581b20ab10a8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 11:11:18 +0100 Subject: [PATCH 035/128] Add some missing trailing newlines --- tests/sublime_plugin.py | 2 +- tests/test_sublime_modelines.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sublime_plugin.py b/tests/sublime_plugin.py index ce340de..6545364 100644 --- a/tests/sublime_plugin.py +++ b/tests/sublime_plugin.py @@ -15,4 +15,4 @@ class TextCommand(Plugin): class EventListener(Plugin): - pass \ No newline at end of file + pass diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index d63bf5a..0022be9 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -154,4 +154,4 @@ def test_to_json_type(): assert sublime_modelines.to_json_type(b) == 1.0 assert sublime_modelines.to_json_type(c) == False assert sublime_modelines.to_json_type(d) == True - assert sublime_modelines.to_json_type(e) == e \ No newline at end of file + assert sublime_modelines.to_json_type(e) == e From dfb5bbad8dbec325d3d9a22ef245f524df56fa6b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 11:14:10 +0100 Subject: [PATCH 036/128] Remove init from tests --- tests/__init__.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index a34d8ff..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from importlib import reload - -from . import test_modelines -reload(test_modelines) - -from .. import sublime_modelines -reload(sublime_modelines) - -from .test_modelines import * From 89aa5b06f36a155e13204253817ddca69b8b9bdc Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 13:33:05 +0100 Subject: [PATCH 037/128] Remove useless command --- Modelines.sublime-commands | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 Modelines.sublime-commands diff --git a/Modelines.sublime-commands b/Modelines.sublime-commands deleted file mode 100644 index 939be46..0000000 --- a/Modelines.sublime-commands +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "caption": "Modelines: Run Tests", - "command": "run_plugin_unittest", - "args": {"module": "Modelines.tests"}, - } -] From f595ee1fd1d7cacd674021ebf4ce008c2e7ff6a2 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 13:52:01 +0100 Subject: [PATCH 038/128] Fix running the tests --- tests/sublime.py | 18 --- tests/sublime_plugin.py | 18 --- tests/test_modelines.py | 14 +- tests/test_sublime_modelines.py | 219 ++++++++++++++++---------------- 4 files changed, 116 insertions(+), 153 deletions(-) delete mode 100644 tests/sublime.py delete mode 100644 tests/sublime_plugin.py diff --git a/tests/sublime.py b/tests/sublime.py deleted file mode 100644 index 798b76a..0000000 --- a/tests/sublime.py +++ /dev/null @@ -1,18 +0,0 @@ -#class View(object): -# pass -# -# -#class RegionSet(object): -# pass -# -# -#class Region(object): -# pass -# -# -#class Window(object): -# pass -# -# -#class Options(object): -# pass diff --git a/tests/sublime_plugin.py b/tests/sublime_plugin.py deleted file mode 100644 index 6545364..0000000 --- a/tests/sublime_plugin.py +++ /dev/null @@ -1,18 +0,0 @@ -class Plugin(object): - pass - - -class ApplicationCommand(Plugin): - pass - - -class WindowCommand(Plugin): - pass - - -class TextCommand(Plugin): - pass - - -class EventListener(Plugin): - pass diff --git a/tests/test_modelines.py b/tests/test_modelines.py index 2737d51..446e1aa 100644 --- a/tests/test_modelines.py +++ b/tests/test_modelines.py @@ -1,8 +1,10 @@ -from sublime_unittest import TestCase +from tempfile import mkstemp +from unittest import TestCase import sublime, os class ModelinesTest(TestCase): + def tearDown(self): if hasattr(self, 'tempfile'): if os.path.exists(self.tempfile): @@ -12,8 +14,8 @@ def _modeline_test(self, lines): import tempfile fd, self.tempfile = mkstemp() - fd.write(lines) - fd.close() + os.write(fd, lines) + os.close(fd) view = sublime.active_window().open_file(self.tempfile) @@ -29,11 +31,11 @@ def test_modelines_1(self): self._modeline_test(lines) def _gen_raw_options_test(self, line, expected): - from .. import sublime_modelines + from Modelines import sublime_modelines if isinstance(line, list): - self.assertEquals([x for x in sublime_modelines.gen_raw_options(line)], expected) + self.assertEqual([x for x in sublime_modelines.gen_raw_options(line)], expected) else: - self.assertEquals([x for x in sublime_modelines.gen_raw_options([line])], expected) + self.assertEqual([x for x in sublime_modelines.gen_raw_options([line])], expected) def test_gen_raw_options_vim_compatibility_1(self): diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 0022be9..13417be 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,157 +1,154 @@ -import unittest -import sys -import os - -import mock - +from unittest import TestCase import sublime - -sys.path.extend([".."]) - -sublime.packagesPath = mock.Mock() -sublime.packagesPath.return_value = "XXX" - - -import sublime_plugin -import sublime_modelines +from Modelines import sublime_modelines -def pytest_funcarg__view(request): - view = mock.Mock() - return view +# Original tests. +class SublimeModelinesTest(TestCase): + def setUp(self): + self.view = sublime.active_window().new_file() + # make sure we have a window to work with + s = sublime.load_settings("Preferences.sublime-settings") + s.set("close_windows_when_empty", False) -def test_get_line_comment_char_Does_meta_info_GetCorrectArgs(view): - sublime_modelines.get_line_comment_char(view) + def tearDown(self): + if self.view: + self.view.set_scratch(True) + self.view.window().focus_view(self.view) + self.view.window().run_command("close_file") + + def test_get_line_comment_char_Does_meta_info_GetCorrectArgs(self): + sublime_modelines.get_line_comment_char(self.view) - actual = view.meta_info.call_args - expected = (("shellVariables", 0), {}) + actual = self.view.meta_info.call_args + expected = (("shellVariables", 0), {}) - assert actual == expected + self.assertEqual(actual, expected) -def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(view): - view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "#"}] + def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(self): + self.view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "#"}] - expected = "#" - actual = sublime_modelines.get_line_comment_char(view) + expected = "#" + actual = sublime_modelines.get_line_comment_char(self.view) - assert expected == actual + self.assertEqual(actual, expected) -def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(view): - view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] + def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(self): + self.view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] - expected = "" - actual = sublime_modelines.get_line_comment_char(view) + expected = "" + actual = sublime_modelines.get_line_comment_char(self.view) - assert expected == actual + self.assertEqual(actual, expected) -def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(view): - view.meta_info.return_value = None + def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(self): + self.view.meta_info.return_value = None - expected = "" - actual = sublime_modelines.get_line_comment_char(view) + expected = "" + actual = sublime_modelines.get_line_comment_char(self.view) - assert expected == actual + self.assertEqual(actual, expected) -def test_build_modeline_prefix_AreDefaultsCorrect(): - actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT - expected = "%s\\s*(st|sublime): " % "TEST", "#" - assert actual == expected + def test_build_modeline_prefix_AreDefaultsCorrect(self): + actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT + expected = "%s\\s*(st|sublime): " % "TEST", "#" + self.assertEqual(actual, expected) -def test_BuildPrefixWithDynamicLineCommentChar(view): - view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "//"}] - expected = "%s\\s*(st|sublime): " % "//" - actual = sublime_modelines.build_modeline_prefix(view) - assert actual == expected + def test_BuildPrefixWithDynamicLineCommentChar(self): + self.view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "//"}] + expected = "%s\\s*(st|sublime): " % "//" + actual = sublime_modelines.build_modeline_prefix(self.view) + assert actual == expected -def test_BuildPrefixWithDefaultLineCommentChar(view): - view.meta_info.return_value = None + def test_BuildPrefixWithDefaultLineCommentChar(self): + #self.view.meta_info.return_value = None - expected = "%s\\s*(st|sublime): " % "#" - actual = sublime_modelines.build_modeline_prefix(view) + expected = "%s\\s*(st|sublime): " % "#" + actual = sublime_modelines.build_modeline_prefix(self.view) - assert expected == actual + self.assertEqual(actual, expected) -def test_gen_modelines(view): - sublime.Region = mock.Mock() - view.substr.side_effect = lambda x: x - view.size.return_value = 0 - view.lines.return_value = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline", - "random stuff" - ] - modelines = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline" - ] * 2 # the buffer is so small that there's overlap top/bottom modelines. + def test_gen_modelines(self): + sublime.Region = mock.Mock() + self.view.substr.side_effect = lambda x: x + self.view.size.return_value = 0 + self.view.lines.return_value = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline", + "random stuff" + ] + modelines = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline" + ] * 2 # the buffer is so small that there's overlap top/bottom modelines. - assert modelines == [l for l in sublime_modelines.gen_modelines(view)] + self.assertEqual([l for l in sublime_modelines.gen_modelines(self.view)], modelines) -def test_gen_raw_options(): - mdls = [ - "# sublime: foo bar", - "# sublime: bar foo; foo bar", - "# st: baz foob", - "# st: fibz zap; zup blah" - ] + def test_gen_raw_options(self): + mdls = [ + "# sublime: foo bar", + "# sublime: bar foo; foo bar", + "# st: baz foob", + "# st: fibz zap; zup blah" + ] - actual = [ - "foo bar", - "bar foo", - "foo bar", - "baz foob", - "fibz zap", - "zup blah", - ] + actual = [ + "foo bar", + "bar foo", + "foo bar", + "baz foob", + "fibz zap", + "zup blah", + ] - assert actual == [x for x in sublime_modelines.gen_raw_options(mdls)] + self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) -def test_gen_modeline_options(view): - set = view.settings().set + def test_gen_modeline_options(self): + set = self.view.settings().set - gen_modelines = mock.Mock() - gen_modelines.return_value = ["# sublime: foo bar", - "# sublime: baz zoom"] + gen_modelines = mock.Mock() + gen_modelines.return_value = ["# sublime: foo bar", + "# sublime: baz zoom"] - gen_raw_options = mock.Mock() - gen_raw_options.return_value = ["foo bar", - "baz zoom"] + gen_raw_options = mock.Mock() + gen_raw_options.return_value = ["foo bar", + "baz zoom"] - sublime_modelines.gen_modelines = gen_modelines - sublime_modelines.gen_raw_options = gen_raw_options + sublime_modelines.gen_modelines = gen_modelines + sublime_modelines.gen_raw_options = gen_raw_options - actual = [x for x in sublime_modelines.gen_modeline_options(view)] - assert [(set, "foo", "bar"), (set, "baz", "zoom")] == actual + actual = [x for x in sublime_modelines.gen_modeline_options(self.view)] + self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) -def test_is_modeline(view): - sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") - view.substr.return_value = "# sublime: " - assert sublime_modelines.is_modeline(view, 0) + def test_is_modeline(self): + sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") + self.view.substr.return_value = "# sublime: " + self.assertTrue(sublime_modelines.is_modeline(self.view, 0)) -def test_to_json_type(): - a = "1" - b = "1.0" - c = "false" - d = "true" - e = list() + def test_to_json_type(self): + a = "1" + b = "1.0" + c = "false" + d = "true" + e = list() - assert sublime_modelines.to_json_type(a) == 1 - assert sublime_modelines.to_json_type(b) == 1.0 - assert sublime_modelines.to_json_type(c) == False - assert sublime_modelines.to_json_type(d) == True - assert sublime_modelines.to_json_type(e) == e + self.assertEqual(sublime_modelines.to_json_type(a), 1) + self.assertEqual(sublime_modelines.to_json_type(b), 1.0) + self.assertEqual(sublime_modelines.to_json_type(c), False) + self.assertEqual(sublime_modelines.to_json_type(d), True) + self.assertEqual(sublime_modelines.to_json_type(e), e) From 4f7359ca889da7751f6a49dc15cf435f94a67f3f Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 14:36:38 +0100 Subject: [PATCH 039/128] Continue test fixes --- tests/test_modelines.py | 2 ++ tests/test_sublime_modelines.py | 41 ++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/test_modelines.py b/tests/test_modelines.py index 446e1aa..6ddb7c9 100644 --- a/tests/test_modelines.py +++ b/tests/test_modelines.py @@ -1,3 +1,5 @@ +# This is the test file that was added with ST 3 compatibility. + from tempfile import mkstemp from unittest import TestCase import sublime, os diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 13417be..64d1f90 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,15 +1,34 @@ +# This is the original test file before ST 3 compatibility was added. + +from typing import Any from unittest import TestCase import sublime from Modelines import sublime_modelines -# Original tests. +class MockView(View): + + comment_start_char: str|None = None + latest_meta_info_call_args: tuple[tuple[str, Point], Any]|None = None + + def set_comment_start_char(self, new_char: str|None): + self.comment_start_char = new_char + + def meta_info(self, key: str, pt: Point): + res = None + if key != "TM_COMMENT_START" or self.comment_start_char == None: res = super.meta_info(key, pt) + else: res = self.comment_start_char + self.latest_meta_info_call_args = ((key, pt), res) + return res + class SublimeModelinesTest(TestCase): def setUp(self): - self.view = sublime.active_window().new_file() - # make sure we have a window to work with + self.view = sublime.active_window().new_file(NewFileFlags.TRANSIENT, "text") + self.view.__class__ = MockView + + # Make sure we have a window to work with. s = sublime.load_settings("Preferences.sublime-settings") s.set("close_windows_when_empty", False) @@ -19,17 +38,17 @@ def tearDown(self): self.view.window().focus_view(self.view) self.view.window().run_command("close_file") - def test_get_line_comment_char_Does_meta_info_GetCorrectArgs(self): + def test_get_line_comment_char_does_meta_info_with_correct_args_and_get_correct_result(self): sublime_modelines.get_line_comment_char(self.view) actual = self.view.meta_info.call_args - expected = (("shellVariables", 0), {}) + expected = (("TM_COMMENT_START", 0), "") self.assertEqual(actual, expected) def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(self): - self.view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "#"}] + self.view.set_comment_start_char("#") expected = "#" actual = sublime_modelines.get_line_comment_char(self.view) @@ -57,13 +76,13 @@ def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(se def test_build_modeline_prefix_AreDefaultsCorrect(self): actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT - expected = "%s\\s*(st|sublime): " % "TEST", "#" + expected = "%s\\s*(st|sublime):" % "TEST", "#" self.assertEqual(actual, expected) - def test_BuildPrefixWithDynamicLineCommentChar(self): - self.view.meta_info.return_value = [{ "name": "TM_COMMENT_START", "value": "//"}] - expected = "%s\\s*(st|sublime): " % "//" + def test_BuildPrefixWithDynamicLineCommentDoubleSlash(self): + self.view.set = [{"name": "TM_COMMENT_START", "value": "//"}] + expected = "%s\\s*(st|sublime):" % "//" actual = sublime_modelines.build_modeline_prefix(self.view) assert actual == expected @@ -71,7 +90,7 @@ def test_BuildPrefixWithDynamicLineCommentChar(self): def test_BuildPrefixWithDefaultLineCommentChar(self): #self.view.meta_info.return_value = None - expected = "%s\\s*(st|sublime): " % "#" + expected = "%s\\s*(st|sublime):" % "#" actual = sublime_modelines.build_modeline_prefix(self.view) self.assertEqual(actual, expected) From 79c0eded2f4e30d0a6b4b63fd426f2d9ef595fb3 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 14:37:39 +0100 Subject: [PATCH 040/128] Remove type hints in tests as they are not supported by UnitTesting --- tests/test_sublime_modelines.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 64d1f90..3710dd1 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,6 +1,5 @@ # This is the original test file before ST 3 compatibility was added. -from typing import Any from unittest import TestCase import sublime @@ -9,13 +8,13 @@ class MockView(View): - comment_start_char: str|None = None - latest_meta_info_call_args: tuple[tuple[str, Point], Any]|None = None + comment_start_char = None + latest_meta_info_call_args = None - def set_comment_start_char(self, new_char: str|None): + def set_comment_start_char(self, new_char): self.comment_start_char = new_char - def meta_info(self, key: str, pt: Point): + def meta_info(self, key, pt): res = None if key != "TM_COMMENT_START" or self.comment_start_char == None: res = super.meta_info(key, pt) else: res = self.comment_start_char From 6bad26bc9075a688f095be060f4656f8a5c29d57 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 14:50:07 +0100 Subject: [PATCH 041/128] Fix test compilation --- tests/test_sublime_modelines.py | 218 ++++++++++++++++---------------- 1 file changed, 110 insertions(+), 108 deletions(-) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 3710dd1..3298aae 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -6,7 +6,8 @@ from Modelines import sublime_modelines -class MockView(View): + +class MockView(sublime.View): comment_start_char = None latest_meta_info_call_args = None @@ -16,15 +17,16 @@ def set_comment_start_char(self, new_char): def meta_info(self, key, pt): res = None - if key != "TM_COMMENT_START" or self.comment_start_char == None: res = super.meta_info(key, pt) + if key != "TM_COMMENT_START" or self.comment_start_char == None: res = super().meta_info(key, pt) else: res = self.comment_start_char self.latest_meta_info_call_args = ((key, pt), res) return res + class SublimeModelinesTest(TestCase): def setUp(self): - self.view = sublime.active_window().new_file(NewFileFlags.TRANSIENT, "text") + self.view = sublime.active_window().new_file(sublime.TRANSIENT, "") self.view.__class__ = MockView # Make sure we have a window to work with. @@ -40,133 +42,133 @@ def tearDown(self): def test_get_line_comment_char_does_meta_info_with_correct_args_and_get_correct_result(self): sublime_modelines.get_line_comment_char(self.view) - actual = self.view.meta_info.call_args + actual = self.view.latest_meta_info_call_args expected = (("TM_COMMENT_START", 0), "") self.assertEqual(actual, expected) - def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(self): - self.view.set_comment_start_char("#") + # def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(self): + # self.view.set_comment_start_char("#") - expected = "#" - actual = sublime_modelines.get_line_comment_char(self.view) + # expected = "#" + # actual = sublime_modelines.get_line_comment_char(self.view) - self.assertEqual(actual, expected) + # self.assertEqual(actual, expected) - def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(self): - self.view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] + # def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(self): + # self.view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] - expected = "" - actual = sublime_modelines.get_line_comment_char(self.view) + # expected = "" + # actual = sublime_modelines.get_line_comment_char(self.view) - self.assertEqual(actual, expected) + # self.assertEqual(actual, expected) - def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(self): - self.view.meta_info.return_value = None + # def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(self): + # self.view.meta_info.return_value = None - expected = "" - actual = sublime_modelines.get_line_comment_char(self.view) + # expected = "" + # actual = sublime_modelines.get_line_comment_char(self.view) - self.assertEqual(actual, expected) + # self.assertEqual(actual, expected) - def test_build_modeline_prefix_AreDefaultsCorrect(self): - actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT - expected = "%s\\s*(st|sublime):" % "TEST", "#" - self.assertEqual(actual, expected) + # def test_build_modeline_prefix_AreDefaultsCorrect(self): + # actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT + # expected = "%s\\s*(st|sublime):" % "TEST", "#" + # self.assertEqual(actual, expected) - def test_BuildPrefixWithDynamicLineCommentDoubleSlash(self): - self.view.set = [{"name": "TM_COMMENT_START", "value": "//"}] - expected = "%s\\s*(st|sublime):" % "//" - actual = sublime_modelines.build_modeline_prefix(self.view) - assert actual == expected + # def test_BuildPrefixWithDynamicLineCommentDoubleSlash(self): + # self.view.set = [{"name": "TM_COMMENT_START", "value": "//"}] + # expected = "%s\\s*(st|sublime):" % "//" + # actual = sublime_modelines.build_modeline_prefix(self.view) + # assert actual == expected - def test_BuildPrefixWithDefaultLineCommentChar(self): - #self.view.meta_info.return_value = None + # def test_BuildPrefixWithDefaultLineCommentChar(self): + # #self.view.meta_info.return_value = None - expected = "%s\\s*(st|sublime):" % "#" - actual = sublime_modelines.build_modeline_prefix(self.view) + # expected = "%s\\s*(st|sublime):" % "#" + # actual = sublime_modelines.build_modeline_prefix(self.view) + + # self.assertEqual(actual, expected) + + + # def test_gen_modelines(self): + # sublime.Region = mock.Mock() + # self.view.substr.side_effect = lambda x: x + # self.view.size.return_value = 0 + # self.view.lines.return_value = [ + # "# sublime: hello world", + # "# sublime: hi there; it's me", + # "#sublime: some modeline", + # "random stuff" + # ] + # modelines = [ + # "# sublime: hello world", + # "# sublime: hi there; it's me", + # "#sublime: some modeline" + # ] * 2 # the buffer is so small that there's overlap top/bottom modelines. + + # self.assertEqual([l for l in sublime_modelines.gen_modelines(self.view)], modelines) + + + # def test_gen_raw_options(self): + # mdls = [ + # "# sublime: foo bar", + # "# sublime: bar foo; foo bar", + # "# st: baz foob", + # "# st: fibz zap; zup blah" + # ] + + # actual = [ + # "foo bar", + # "bar foo", + # "foo bar", + # "baz foob", + # "fibz zap", + # "zup blah", + # ] + + # self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) + + + # def test_gen_modeline_options(self): + # set = self.view.settings().set + + # gen_modelines = mock.Mock() + # gen_modelines.return_value = ["# sublime: foo bar", + # "# sublime: baz zoom"] + + # gen_raw_options = mock.Mock() + # gen_raw_options.return_value = ["foo bar", + # "baz zoom"] + + # sublime_modelines.gen_modelines = gen_modelines + # sublime_modelines.gen_raw_options = gen_raw_options + + # actual = [x for x in sublime_modelines.gen_modeline_options(self.view)] + # self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) + + + # def test_is_modeline(self): + # sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") + # self.view.substr.return_value = "# sublime: " + # self.assertTrue(sublime_modelines.is_modeline(self.view, 0)) - self.assertEqual(actual, expected) + # def test_to_json_type(self): + # a = "1" + # b = "1.0" + # c = "false" + # d = "true" + # e = list() - def test_gen_modelines(self): - sublime.Region = mock.Mock() - self.view.substr.side_effect = lambda x: x - self.view.size.return_value = 0 - self.view.lines.return_value = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline", - "random stuff" - ] - modelines = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline" - ] * 2 # the buffer is so small that there's overlap top/bottom modelines. - - self.assertEqual([l for l in sublime_modelines.gen_modelines(self.view)], modelines) - - - def test_gen_raw_options(self): - mdls = [ - "# sublime: foo bar", - "# sublime: bar foo; foo bar", - "# st: baz foob", - "# st: fibz zap; zup blah" - ] - - actual = [ - "foo bar", - "bar foo", - "foo bar", - "baz foob", - "fibz zap", - "zup blah", - ] - - self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) - - - def test_gen_modeline_options(self): - set = self.view.settings().set - - gen_modelines = mock.Mock() - gen_modelines.return_value = ["# sublime: foo bar", - "# sublime: baz zoom"] - - gen_raw_options = mock.Mock() - gen_raw_options.return_value = ["foo bar", - "baz zoom"] - - sublime_modelines.gen_modelines = gen_modelines - sublime_modelines.gen_raw_options = gen_raw_options - - actual = [x for x in sublime_modelines.gen_modeline_options(self.view)] - self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) - - - def test_is_modeline(self): - sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") - self.view.substr.return_value = "# sublime: " - self.assertTrue(sublime_modelines.is_modeline(self.view, 0)) - - - def test_to_json_type(self): - a = "1" - b = "1.0" - c = "false" - d = "true" - e = list() - - self.assertEqual(sublime_modelines.to_json_type(a), 1) - self.assertEqual(sublime_modelines.to_json_type(b), 1.0) - self.assertEqual(sublime_modelines.to_json_type(c), False) - self.assertEqual(sublime_modelines.to_json_type(d), True) - self.assertEqual(sublime_modelines.to_json_type(e), e) + # self.assertEqual(sublime_modelines.to_json_type(a), 1) + # self.assertEqual(sublime_modelines.to_json_type(b), 1.0) + # self.assertEqual(sublime_modelines.to_json_type(c), False) + # self.assertEqual(sublime_modelines.to_json_type(d), True) + # self.assertEqual(sublime_modelines.to_json_type(e), e) From b001b0febbbfe56edf77c67b3e13f031e5cbec27 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 15:35:07 +0100 Subject: [PATCH 042/128] Change a test to make sure we do not call meta_info anymore --- tests/test_sublime_modelines.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 3298aae..affc22f 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -39,11 +39,14 @@ def tearDown(self): self.view.window().focus_view(self.view) self.view.window().run_command("close_file") - def test_get_line_comment_char_does_meta_info_with_correct_args_and_get_correct_result(self): + # This test is strange, but it relates to a previous version of Modelines checking the comment char to make it a part of the regex to detect modelines. + # We do not do that anymore; let’s make sure of it! + # (I like the mock thing I did, I don’t want to remove it…) + def test_get_line_comment_char_does_not_call_meta_info(self): sublime_modelines.get_line_comment_char(self.view) actual = self.view.latest_meta_info_call_args - expected = (("TM_COMMENT_START", 0), "") + expected = None self.assertEqual(actual, expected) From fb9ff7f33396ca02db3a5d6b4d7941a4b20eeee3 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 15:39:26 +0100 Subject: [PATCH 043/128] Fix test_get_line_comment_char_does_not_call_meta_info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s meaningless, but the test pass! --- sublime_modelines.py | 22 +--------------------- tests/test_sublime_modelines.py | 30 +----------------------------- 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index 15126a2..fad3e73 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -145,28 +145,8 @@ def _setter(n,v): yield _setter, name, value -def get_line_comment_char(view): - commentChar = "" - commentChar2 = "" - try: - for pair in view.meta_info("shellVariables", 0): - if pair["name"] == "TM_COMMENT_START": - commentChar = pair["value"] - if pair["name"] == "TM_COMMENT_START_2": - commentChar2 = pair["value"] - if commentChar and commentChar2: - break - except TypeError: - pass - - if not commentChar2: - return re.escape(commentChar.strip()) - else: - return "(" + re.escape(commentChar.strip()) + "|" + re.escape(commentChar2.strip()) + ")" - def build_modeline_prefix(view): - lineComment = get_line_comment_char(view).lstrip() or DEFAULT_LINE_COMMENT - return (MODELINE_PREFIX_TPL % lineComment) + return (MODELINE_PREFIX_TPL % DEFAULT_LINE_COMMENT) def to_json_type(v): diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index affc22f..5f892f9 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -43,41 +43,13 @@ def tearDown(self): # We do not do that anymore; let’s make sure of it! # (I like the mock thing I did, I don’t want to remove it…) def test_get_line_comment_char_does_not_call_meta_info(self): - sublime_modelines.get_line_comment_char(self.view) + sublime_modelines.build_modeline_prefix(self.view) actual = self.view.latest_meta_info_call_args expected = None self.assertEqual(actual, expected) - - # def test_get_line_comment_char_DoWeGetLineCommentCharIfExists(self): - # self.view.set_comment_start_char("#") - - # expected = "#" - # actual = sublime_modelines.get_line_comment_char(self.view) - - # self.assertEqual(actual, expected) - - - # def test_get_line_comment_char_DoWeGetEmptyLineIfLineCommentCharDoesntExist(self): - # self.view.meta_info.return_value = [{ "name": "NOT_TM_COMMENT_START", "value": "#"}] - - # expected = "" - # actual = sublime_modelines.get_line_comment_char(self.view) - - # self.assertEqual(actual, expected) - - - # def test_get_line_comment_char_ShouldReturnEmptyStringIfNoExtraVariablesExist(self): - # self.view.meta_info.return_value = None - - # expected = "" - # actual = sublime_modelines.get_line_comment_char(self.view) - - # self.assertEqual(actual, expected) - - # def test_build_modeline_prefix_AreDefaultsCorrect(self): # actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT # expected = "%s\\s*(st|sublime):" % "TEST", "#" From 40e1935e8079473daf5bbd0c9436cfa7c2ad4ff0 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 23 Jan 2026 15:47:11 +0100 Subject: [PATCH 044/128] Fix test_to_json_type --- sublime_modelines.py | 9 +++++---- tests/test_sublime_modelines.py | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/sublime_modelines.py b/sublime_modelines.py index fad3e73..660a858 100644 --- a/sublime_modelines.py +++ b/sublime_modelines.py @@ -150,11 +150,12 @@ def build_modeline_prefix(view): def to_json_type(v): - """"Convert string value to proper JSON type. - """ + """Convert string value to proper JSON type.""" + if not isinstance(v, str): + return json.loads(json.dumps(v)) + try: - result = json.loads(v.strip()) - return result + return json.loads(v.strip()) except Exception as e: if v: if v[0] not in "[{": diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 5f892f9..f18a15e 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -135,15 +135,15 @@ def test_get_line_comment_char_does_not_call_meta_info(self): # self.assertTrue(sublime_modelines.is_modeline(self.view, 0)) - # def test_to_json_type(self): - # a = "1" - # b = "1.0" - # c = "false" - # d = "true" - # e = list() - - # self.assertEqual(sublime_modelines.to_json_type(a), 1) - # self.assertEqual(sublime_modelines.to_json_type(b), 1.0) - # self.assertEqual(sublime_modelines.to_json_type(c), False) - # self.assertEqual(sublime_modelines.to_json_type(d), True) - # self.assertEqual(sublime_modelines.to_json_type(e), e) + def test_to_json_type(self): + a = "1" + b = "1.0" + c = "false" + d = "true" + e = list() + + self.assertEqual(sublime_modelines.to_json_type(a), 1) + self.assertEqual(sublime_modelines.to_json_type(b), 1.0) + self.assertEqual(sublime_modelines.to_json_type(c), False) + self.assertEqual(sublime_modelines.to_json_type(d), True) + self.assertEqual(sublime_modelines.to_json_type(e), e) From 5680c0c9b494e9d747d6913b27311d73937bd105 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 01:45:10 +0100 Subject: [PATCH 045/128] Enable a new test, remove dead code --- tests/test_sublime_modelines.py | 61 +++++++++++---------------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index f18a15e..c5e714d 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -39,7 +39,8 @@ def tearDown(self): self.view.window().focus_view(self.view) self.view.window().run_command("close_file") - # This test is strange, but it relates to a previous version of Modelines checking the comment char to make it a part of the regex to detect modelines. + # This test is strange, but it relates to a previous version of Modelines + # that used to check the comment char to make it a part of the regex to detect modelines. # We do not do that anymore; let’s make sure of it! # (I like the mock thing I did, I don’t want to remove it…) def test_get_line_comment_char_does_not_call_meta_info(self): @@ -50,28 +51,6 @@ def test_get_line_comment_char_does_not_call_meta_info(self): self.assertEqual(actual, expected) - # def test_build_modeline_prefix_AreDefaultsCorrect(self): - # actual = sublime_modelines.MODELINE_PREFIX_TPL % "TEST", sublime_modelines.DEFAULT_LINE_COMMENT - # expected = "%s\\s*(st|sublime):" % "TEST", "#" - # self.assertEqual(actual, expected) - - - # def test_BuildPrefixWithDynamicLineCommentDoubleSlash(self): - # self.view.set = [{"name": "TM_COMMENT_START", "value": "//"}] - # expected = "%s\\s*(st|sublime):" % "//" - # actual = sublime_modelines.build_modeline_prefix(self.view) - # assert actual == expected - - - # def test_BuildPrefixWithDefaultLineCommentChar(self): - # #self.view.meta_info.return_value = None - - # expected = "%s\\s*(st|sublime):" % "#" - # actual = sublime_modelines.build_modeline_prefix(self.view) - - # self.assertEqual(actual, expected) - - # def test_gen_modelines(self): # sublime.Region = mock.Mock() # self.view.substr.side_effect = lambda x: x @@ -91,24 +70,24 @@ def test_get_line_comment_char_does_not_call_meta_info(self): # self.assertEqual([l for l in sublime_modelines.gen_modelines(self.view)], modelines) - # def test_gen_raw_options(self): - # mdls = [ - # "# sublime: foo bar", - # "# sublime: bar foo; foo bar", - # "# st: baz foob", - # "# st: fibz zap; zup blah" - # ] - - # actual = [ - # "foo bar", - # "bar foo", - # "foo bar", - # "baz foob", - # "fibz zap", - # "zup blah", - # ] - - # self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) + def test_gen_raw_options(self): + mdls = [ + "# sublime: foo bar", + "# sublime: bar foo; foo bar", + "# st: baz foob", + "# st: fibz zap; zup blah", + ] + + actual = [ + "foo bar", + "bar foo", + "foo bar", + "baz foob", + "fibz zap", + "zup blah", + ] + + self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) # def test_gen_modeline_options(self): From 42823fdc6866f141f0dbe356fc0625b40fa2a424 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 02:18:49 +0100 Subject: [PATCH 046/128] Finish migrating old original tests --- tests/test_sublime_modelines.py | 148 ++++++++++++++------------------ 1 file changed, 64 insertions(+), 84 deletions(-) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index c5e714d..bb36fba 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,75 +1,52 @@ # This is the original test file before ST 3 compatibility was added. from unittest import TestCase +from unittest.mock import Mock import sublime from Modelines import sublime_modelines -class MockView(sublime.View): - - comment_start_char = None - latest_meta_info_call_args = None - - def set_comment_start_char(self, new_char): - self.comment_start_char = new_char - - def meta_info(self, key, pt): - res = None - if key != "TM_COMMENT_START" or self.comment_start_char == None: res = super().meta_info(key, pt) - else: res = self.comment_start_char - self.latest_meta_info_call_args = ((key, pt), res) - return res - - class SublimeModelinesTest(TestCase): - - def setUp(self): - self.view = sublime.active_window().new_file(sublime.TRANSIENT, "") - self.view.__class__ = MockView - - # Make sure we have a window to work with. - s = sublime.load_settings("Preferences.sublime-settings") - s.set("close_windows_when_empty", False) - - def tearDown(self): - if self.view: - self.view.set_scratch(True) - self.view.window().focus_view(self.view) - self.view.window().run_command("close_file") # This test is strange, but it relates to a previous version of Modelines # that used to check the comment char to make it a part of the regex to detect modelines. # We do not do that anymore; let’s make sure of it! - # (I like the mock thing I did, I don’t want to remove it…) def test_get_line_comment_char_does_not_call_meta_info(self): - sublime_modelines.build_modeline_prefix(self.view) - - actual = self.view.latest_meta_info_call_args + view = Mock() + sublime_modelines.build_modeline_prefix(view) + + actual = view.meta_info.call_args expected = None - + self.assertEqual(actual, expected) - - # def test_gen_modelines(self): - # sublime.Region = mock.Mock() - # self.view.substr.side_effect = lambda x: x - # self.view.size.return_value = 0 - # self.view.lines.return_value = [ - # "# sublime: hello world", - # "# sublime: hi there; it's me", - # "#sublime: some modeline", - # "random stuff" - # ] - # modelines = [ - # "# sublime: hello world", - # "# sublime: hi there; it's me", - # "#sublime: some modeline" - # ] * 2 # the buffer is so small that there's overlap top/bottom modelines. - - # self.assertEqual([l for l in sublime_modelines.gen_modelines(self.view)], modelines) - - + + def test_gen_modelines(self): + # Override the builtin Sublime Region class (with a backup, we restore it at the end of the test). + originalRegion = sublime.Region + sublime.Region = Mock() + + view = Mock() + view.substr.side_effect = lambda x: x + view.size.return_value = 0 + view.lines.return_value = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline", + "random stuff" + ] + modelines = [ + "# sublime: hello world", + "# sublime: hi there; it's me", + "#sublime: some modeline" + ] * 2 # The buffer is so small the top/bottom modelines overlap. + + self.assertEqual([l for l in sublime_modelines.gen_modelines(view)], modelines) + + # Restore the Region class. + sublime.Region = originalRegion + def test_gen_raw_options(self): mdls = [ "# sublime: foo bar", @@ -77,7 +54,6 @@ def test_gen_raw_options(self): "# st: baz foob", "# st: fibz zap; zup blah", ] - actual = [ "foo bar", "bar foo", @@ -86,41 +62,45 @@ def test_gen_raw_options(self): "fibz zap", "zup blah", ] - self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) - - - # def test_gen_modeline_options(self): - # set = self.view.settings().set - - # gen_modelines = mock.Mock() - # gen_modelines.return_value = ["# sublime: foo bar", - # "# sublime: baz zoom"] - - # gen_raw_options = mock.Mock() - # gen_raw_options.return_value = ["foo bar", - # "baz zoom"] - - # sublime_modelines.gen_modelines = gen_modelines - # sublime_modelines.gen_raw_options = gen_raw_options - - # actual = [x for x in sublime_modelines.gen_modeline_options(self.view)] - # self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) - - - # def test_is_modeline(self): - # sublime_modelines.build_modeline_prefix = mock.Mock(return_value="# sublime: ") - # self.view.substr.return_value = "# sublime: " - # self.assertTrue(sublime_modelines.is_modeline(self.view, 0)) - - + + def test_gen_modeline_options(self): + view = Mock() + set = view.settings().set + + gen_modelines = Mock() + gen_modelines.return_value = [ + "# sublime: foo bar", + "# sublime: baz zoom", + ] + + gen_raw_options = Mock() + gen_raw_options.return_value = [ + "foo bar", + "baz zoom", + ] + + original_gen_modelines = sublime_modelines.gen_modelines + original_gen_raw_options = sublime_modelines.gen_raw_options + sublime_modelines.gen_modelines = gen_modelines + sublime_modelines.gen_raw_options = gen_raw_options + + actual = [x for x in sublime_modelines.gen_modeline_options(view)] + self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) + + sublime_modelines.gen_modelines = original_gen_modelines + sublime_modelines.gen_raw_options = original_gen_raw_options + + def test_is_modeline(self): + self.assertTrue(sublime_modelines.is_modeline("# sublime: ", "# sublime: ")) + def test_to_json_type(self): a = "1" b = "1.0" c = "false" d = "true" e = list() - + self.assertEqual(sublime_modelines.to_json_type(a), 1) self.assertEqual(sublime_modelines.to_json_type(b), 1.0) self.assertEqual(sublime_modelines.to_json_type(c), False) From 6120e85507bf0965d8c8cad88993319058e3dd9f Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 02:29:20 +0100 Subject: [PATCH 047/128] Reformat newer test file and remove duplicated test --- tests/test_modelines.py | 119 ++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 66 deletions(-) diff --git a/tests/test_modelines.py b/tests/test_modelines.py index 6ddb7c9..e732a84 100644 --- a/tests/test_modelines.py +++ b/tests/test_modelines.py @@ -8,92 +8,79 @@ class ModelinesTest(TestCase): def tearDown(self): - if hasattr(self, 'tempfile'): + if hasattr(self, "tempfile"): if os.path.exists(self.tempfile): os.remove(self.tempfile) - + def _modeline_test(self, lines): import tempfile - + fd, self.tempfile = mkstemp() os.write(fd, lines) os.close(fd) - + view = sublime.active_window().open_file(self.tempfile) - + while view.is_loading(): yield - - # here test view's settings - + + # here test view’s settings + # in the end remove tempfile - + def test_modelines_1(self): lines = ("# sublime:et:ai:ts=4:\n") self._modeline_test(lines) - + def _gen_raw_options_test(self, line, expected): from Modelines import sublime_modelines - if isinstance(line, list): - self.assertEqual([x for x in sublime_modelines.gen_raw_options(line)], expected) - else: - self.assertEqual([x for x in sublime_modelines.gen_raw_options([line])], expected) - - + if isinstance(line, list): self.assertEqual([x for x in sublime_modelines.gen_raw_options( line )], expected) + else: self.assertEqual([x for x in sublime_modelines.gen_raw_options([line])], expected) + def test_gen_raw_options_vim_compatibility_1(self): - self._gen_raw_options_test("# vim: set ai noet ts=4:", - - [ ('auto_indent', '=', 'true'), - ('translate_tabs_to_spaces', '=', 'false'), - ('tab_size', '=', '4') ] - ) - + self._gen_raw_options_test( + "# vim: set ai noet ts=4:", + [ + ("auto_indent", "=", "true"), + ("translate_tabs_to_spaces", "=", "false"), + ("tab_size", "=", "4"), + ] + ) + def test_gen_raw_options_vim_compatibility_2(self): - self._gen_raw_options_test("# vim:ai:et:ts=4:", - [ ('auto_indent', '=', 'true'), - ('translate_tabs_to_spaces', '=', 'true'), - ('tab_size', '=', '4') ] - ) - + self._gen_raw_options_test( + "# vim:ai:et:ts=4:", + [ + ("auto_indent", "=", "true"), + ("translate_tabs_to_spaces", "=", "true"), + ("tab_size", "=", "4"), + ] + ) + def test_gen_raw_options_vim_compatibility_3(self): - self._gen_raw_options_test('# sublime:ai:et:ts=4:ignored_packages+="Makefile Improved":', - [('auto_indent', '=', 'true'), - ('translate_tabs_to_spaces', '=', 'true'), - ('tab_size', '=', '4'), - ('ignored_packages', '+=', '"Makefile Improved"')] - ) - - + self._gen_raw_options_test( + '# sublime:ai:et:ts=4:ignored_packages+="Makefile Improved":', + [ + ("auto_indent", "=", "true"), + ("translate_tabs_to_spaces", "=", "true"), + ("tab_size", "=", "4"), + ("ignored_packages", "+=", '"Makefile Improved"'), + ] + ) + def test_gen_raw_options_vim_compatibility_4(self): - self._gen_raw_options_test('# sublime:ai:et:ts=4:ignored_packages+=["Makefile Improved", "Vintage"]:', - [('auto_indent', '=', 'true'), - ('translate_tabs_to_spaces', '=', 'true'), - ('tab_size', '=', '4'), - ('ignored_packages', '+=', '["Makefile Improved", "Vintage"]')] - ) - + self._gen_raw_options_test( + '# sublime:ai:et:ts=4:ignored_packages+=["Makefile Improved", "Vintage"]:', + [ + ("auto_indent", "=", "true"), + ("translate_tabs_to_spaces", "=", "true"), + ("tab_size", "=", "4"), + ("ignored_packages", "+=", '["Makefile Improved", "Vintage"]'), + ] + ) + def test_gen_raw_options_vim_compatibility_5(self): - #import spdb ; spdb.start() self._gen_raw_options_test( '# sublime: set color_scheme="Packages/Color Scheme - Default/Monokai.tmTheme":', - [('color_scheme', '=', '"Packages/Color Scheme - Default/Monokai.tmTheme"')]) - - - def test_gen_raw_options(self): - - mdls = [ - "# sublime: foo bar", - "# sublime: bar foo; foo bar", - "# st: baz foob", - "# st: fibz zap; zup blah", - ] - - actual = [ - "foo bar", - "bar foo", - "foo bar", - "baz foob", - "fibz zap", - "zup blah", - ] - self._gen_raw_options_test(mdls, actual) + [("color_scheme", "=", '"Packages/Color Scheme - Default/Monokai.tmTheme"')] + ) From 9499d282d02e216aa1bce892386afce994b429a0 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 22:45:55 +0100 Subject: [PATCH 048/128] Typo fix --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 12a584d..c9d4906 100644 --- a/Readme.md +++ b/Readme.md @@ -74,7 +74,7 @@ or Sets the syntax to the specified `.tmLanguage` file. -# Contributers +# Contributors Kay-Uwe (Kiwi) Lorenz (): - Added VIM compatibility; From 28b5af6d5d19da88dd49dfd1932fa1975437cffc Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 22:46:10 +0100 Subject: [PATCH 049/128] =?UTF-8?q?Add=20Guillermo=20L=C3=B3pez-Anglada=20?= =?UTF-8?q?contribution=20in=20Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Readme.md b/Readme.md index c9d4906..c0387d0 100644 --- a/Readme.md +++ b/Readme.md @@ -76,6 +76,9 @@ Sets the syntax to the specified `.tmLanguage` file. # Contributors +[Guillermo López-Anglada](): +- Implemented the first version of this package (for Sublime Text 2). + Kay-Uwe (Kiwi) Lorenz (): - Added VIM compatibility; - Smart syntax matching; From 46380fb252349118dc9a4426fdfae2d9e5b11f5a Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 22:48:42 +0100 Subject: [PATCH 050/128] =?UTF-8?q?Add=20Guillermo=20L=C3=B3pez-Anglada=20?= =?UTF-8?q?Copyright=20back=20in=20the=20License=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- License.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/License.txt b/License.txt index e43064d..b02fa06 100644 --- a/License.txt +++ b/License.txt @@ -1,4 +1,5 @@ -Copyright (c) 2026 Frizlab +Copyright (c) 2010 Guillermo López-Anglada + (c) 2026 Frizlab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From ceb7d4754d7f417be00a3b17027e607272915484 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 22:52:36 +0100 Subject: [PATCH 051/128] Update URL for Sublime package installation documentation --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index c0387d0..3090881 100644 --- a/Readme.md +++ b/Readme.md @@ -16,7 +16,7 @@ Use Package Control and install `SublimeModelines`. Download and install [SublimeModelines](). -See the [installation instructions](http://sublimetext.info/docs/en/extensibility/packages.html#installation-of-packages) for `.sublime-package`s. +See the [installation instructions]() for `.sublime-package`s. ## Side Effects From 6c71f4ca2c5be57ba555e1f5b13f0c04400a7e2a Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sat, 24 Jan 2026 22:56:32 +0100 Subject: [PATCH 052/128] Add a GitHub workflow to test the package on the CI --- .github/workflows/tests.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..395f08d --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,21 @@ +name: tests + +on: [push, pull_request] + +jobs: + run-tests: + strategy: + fail-fast: false + matrix: + st-version: [3, 4] + os: ["ubuntu-latest", "macOS-latest", "windows-latest"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: SublimeText/UnitTesting/actions/setup@v1 + with: + sublime-text-version: ${{ matrix.st-version }} + - uses: SublimeText/UnitTesting/actions/run-tests@v1 + with: + coverage: true + - uses: codecov/codecov-action@v4 From 8aa5377ae3e09ebf78acafcf5870b1efcac20cba Mon Sep 17 00:00:00 2001 From: Frizlab Date: Sun, 25 Jan 2026 23:59:40 +0100 Subject: [PATCH 053/128] Start splitting source code in multiple files --- app/logger.py | 30 ++++ .../sublime_modelines.py | 26 --- plugin.py | 63 ++++++++ tests/test_sublime_modelines.py | 148 +++++++++--------- 4 files changed, 168 insertions(+), 99 deletions(-) create mode 100644 app/logger.py rename sublime_modelines.py => app/sublime_modelines.py (92%) create mode 100644 plugin.py diff --git a/app/logger.py b/app/logger.py new file mode 100644 index 0000000..e88c2bc --- /dev/null +++ b/app/logger.py @@ -0,0 +1,30 @@ +import sys + + + +class Logger: + """A simple logger.""" + + # Default config for the logger. + log_to_tmp = False + enable_debug_log = False + + def __init__(self): + super().__init__() + + def debug(self, s, *args): + if not self.enable_debug_log: + return + self._log(self._format(s, *args)) + + def info(self, s, *args): + self._log(self._format(s, *args)) + + def _format(self, s, *args): + return "[SublimeModelines] " + (s % args) + "\n" + + def _log(self, str): + if self.log_to_tmp: + with open("/tmp/sublime_modelines_debug.log", "a") as myfile: + myfile.write(str) + sys.stderr.write(str) diff --git a/sublime_modelines.py b/app/sublime_modelines.py similarity index 92% rename from sublime_modelines.py rename to app/sublime_modelines.py index 660a858..8348725 100644 --- a/sublime_modelines.py +++ b/app/sublime_modelines.py @@ -1,24 +1,6 @@ -import sublime, sublime_plugin import re, sys, json, os -enable_debug_log = False -enable_log_to_tmp = False - -def log_to_file(str): - if enable_log_to_tmp: - with open("/tmp/modelines_debug.log", "a") as myfile: - myfile.write(str + "\n") - -def log_to_console(s, *args): - log_to_file("[SublimeModelines] "+(s % args)) - sys.stderr.write("[SublimeModelines] " + (s % args) + "\n") - -def debug_log(s, *args): - if enable_debug_log: - log_to_console(s, *args) - - debug_log("Modelines plugin start.") @@ -181,14 +163,6 @@ class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): def __init__(self): self._modes = {} - def on_load(self, view): - debug_log("on_load") - self.do_modelines(view) - - def on_post_save(self, view): - debug_log("on_post_save") - self.do_modelines(view) - def do_modelines(self, view): if not self._modes: self.init_syntax_files() diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..95da120 --- /dev/null +++ b/plugin.py @@ -0,0 +1,63 @@ +import sublime, sublime_plugin +from .app.logger import Logger + + + +class SublimeModelinesPlugin(sublime_plugin.EventListener): + """ + This plugin provides a feature similar to vim modelines, + which allow setting options local to the view by declaring them in the source code file itself. + + A special token is searched in the source code, which declares a modeline (see later for more info about the token). + + The top as well as the bottom of the buffer is scanned for modelines + (`MAX_LINES_TO_CHECK * LINE_LENGTH` defines the size of the regions to be scanned). + + For example, at the end or the beginning of a Python source file, one may find: + ```python + # sublime: gutter false; translate_tab_to_spaces true + ``` + + Token formats: + + - `^\\s*(sublime|st):\\s*key1\\s+val1(\\s*;\\s*keyn\\s+valn)\\s*;?` + - `.{1,7}~~\\s(sublime|st):\\s*key1\\s+val1(\\s*;\\s*keyn\\s+valn)\\s*;?\\s*~~` + + The first format works well if you do not change the syntax of the file. + If you do it is recommended to use the second format + (because the “comment char” is unknown and will thus default to `#`, which may not work for the syntax you need). + + The second format assumes the comment marker (beginning of the line) will have between 1 and 7 characters. + + Also the first format does not really work with `/**/`-style comments as the trailing `*/` will be parsed if it is on the same line as the `/*`. + + All the keys are guaranteed to never have any space, so there are never any ambiguities parsing them. + For the values, to have a semicolon inside, you can escape it by doubling it. + Having a space in the value is ok, except at the beginning or the end, because they will be trimmed. + (It is _not_ possible at all to have a value with one or more spaces at the beginning or the end.) + + When using the second format, values cannot contain a `~~` either. + + Examples: + + - `# sublime: key1 value1; key2 value with space ; key3 hello;;semicolon!;;; key4 last one;` + -> `["key1": "value1", "key2": "value with space", "key3": "hello;semicolon!;" "key4": "last one"]` + - `/*~~ sublime: key1 hello;;semicolon and~~tilde key2 is this parsed? */` + -> `["key1": "hello;semicolon and"]` + """ + + logger = Logger() + + def __init__(self): + super().__init__() + self.logger.log_to_tmp = True + self.logger.enable_debug_log = True + self.logger.debug("Plugin init.") + + def on_load(self, view): + self.logger.debug("on_load called.") + #self.do_modelines(view) + + def on_post_save(self, view): + self.logger.debug("on_post_save called.") + #self.do_modelines(view) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index bb36fba..a5c63f1 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -4,7 +4,7 @@ from unittest.mock import Mock import sublime -from Modelines import sublime_modelines +from Modelines import plugin @@ -15,94 +15,96 @@ class SublimeModelinesTest(TestCase): # We do not do that anymore; let’s make sure of it! def test_get_line_comment_char_does_not_call_meta_info(self): view = Mock() - sublime_modelines.build_modeline_prefix(view) + #sublime_modelines.build_modeline_prefix(view) + + plugin.SublimeModelinesPlugin().on_load(view) actual = view.meta_info.call_args expected = None self.assertEqual(actual, expected) - def test_gen_modelines(self): - # Override the builtin Sublime Region class (with a backup, we restore it at the end of the test). - originalRegion = sublime.Region - sublime.Region = Mock() +# def test_gen_modelines(self): +# # Override the builtin Sublime Region class (with a backup, we restore it at the end of the test). +# originalRegion = sublime.Region +# sublime.Region = Mock() - view = Mock() - view.substr.side_effect = lambda x: x - view.size.return_value = 0 - view.lines.return_value = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline", - "random stuff" - ] - modelines = [ - "# sublime: hello world", - "# sublime: hi there; it's me", - "#sublime: some modeline" - ] * 2 # The buffer is so small the top/bottom modelines overlap. +# view = Mock() +# view.substr.side_effect = lambda x: x +# view.size.return_value = 0 +# view.lines.return_value = [ +# "# sublime: hello world", +# "# sublime: hi there; it's me", +# "#sublime: some modeline", +# "random stuff" +# ] +# modelines = [ +# "# sublime: hello world", +# "# sublime: hi there; it's me", +# "#sublime: some modeline" +# ] * 2 # The buffer is so small the top/bottom modelines overlap. - self.assertEqual([l for l in sublime_modelines.gen_modelines(view)], modelines) +# self.assertEqual([l for l in sublime_modelines.gen_modelines(view)], modelines) - # Restore the Region class. - sublime.Region = originalRegion +# # Restore the Region class. +# sublime.Region = originalRegion - def test_gen_raw_options(self): - mdls = [ - "# sublime: foo bar", - "# sublime: bar foo; foo bar", - "# st: baz foob", - "# st: fibz zap; zup blah", - ] - actual = [ - "foo bar", - "bar foo", - "foo bar", - "baz foob", - "fibz zap", - "zup blah", - ] - self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) +# def test_gen_raw_options(self): +# mdls = [ +# "# sublime: foo bar", +# "# sublime: bar foo; foo bar", +# "# st: baz foob", +# "# st: fibz zap; zup blah", +# ] +# actual = [ +# "foo bar", +# "bar foo", +# "foo bar", +# "baz foob", +# "fibz zap", +# "zup blah", +# ] +# self.assertEqual([x for x in sublime_modelines.gen_raw_options(mdls)], actual) - def test_gen_modeline_options(self): - view = Mock() - set = view.settings().set +# def test_gen_modeline_options(self): +# view = Mock() +# set = view.settings().set - gen_modelines = Mock() - gen_modelines.return_value = [ - "# sublime: foo bar", - "# sublime: baz zoom", - ] +# gen_modelines = Mock() +# gen_modelines.return_value = [ +# "# sublime: foo bar", +# "# sublime: baz zoom", +# ] - gen_raw_options = Mock() - gen_raw_options.return_value = [ - "foo bar", - "baz zoom", - ] +# gen_raw_options = Mock() +# gen_raw_options.return_value = [ +# "foo bar", +# "baz zoom", +# ] - original_gen_modelines = sublime_modelines.gen_modelines - original_gen_raw_options = sublime_modelines.gen_raw_options - sublime_modelines.gen_modelines = gen_modelines - sublime_modelines.gen_raw_options = gen_raw_options +# original_gen_modelines = sublime_modelines.gen_modelines +# original_gen_raw_options = sublime_modelines.gen_raw_options +# sublime_modelines.gen_modelines = gen_modelines +# sublime_modelines.gen_raw_options = gen_raw_options - actual = [x for x in sublime_modelines.gen_modeline_options(view)] - self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) +# actual = [x for x in sublime_modelines.gen_modeline_options(view)] +# self.assertEqual([(set, "foo", "bar"), (set, "baz", "zoom")], actual) - sublime_modelines.gen_modelines = original_gen_modelines - sublime_modelines.gen_raw_options = original_gen_raw_options +# sublime_modelines.gen_modelines = original_gen_modelines +# sublime_modelines.gen_raw_options = original_gen_raw_options - def test_is_modeline(self): - self.assertTrue(sublime_modelines.is_modeline("# sublime: ", "# sublime: ")) +# def test_is_modeline(self): +# self.assertTrue(sublime_modelines.is_modeline("# sublime: ", "# sublime: ")) - def test_to_json_type(self): - a = "1" - b = "1.0" - c = "false" - d = "true" - e = list() +# def test_to_json_type(self): +# a = "1" +# b = "1.0" +# c = "false" +# d = "true" +# e = list() - self.assertEqual(sublime_modelines.to_json_type(a), 1) - self.assertEqual(sublime_modelines.to_json_type(b), 1.0) - self.assertEqual(sublime_modelines.to_json_type(c), False) - self.assertEqual(sublime_modelines.to_json_type(d), True) - self.assertEqual(sublime_modelines.to_json_type(e), e) +# self.assertEqual(sublime_modelines.to_json_type(a), 1) +# self.assertEqual(sublime_modelines.to_json_type(b), 1.0) +# self.assertEqual(sublime_modelines.to_json_type(c), False) +# self.assertEqual(sublime_modelines.to_json_type(d), True) +# self.assertEqual(sublime_modelines.to_json_type(e), e) From b56d6de1697446f090ab1003420b5de587b5ba72 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 04:40:08 +0100 Subject: [PATCH 054/128] Add settings --- .python-version | 1 + Default.sublime-commands | 10 + Main.sublime-menu | 22 ++ Sublime Modelines.sublime-settings | 13 ++ app/__init__.py | 9 + app/logger.py | 32 ++- app/settings.py | 43 ++++ app/sublime_modelines.py | 362 ++++++++++++++--------------- plugin.py | 20 +- 9 files changed, 312 insertions(+), 200 deletions(-) create mode 100644 .python-version create mode 100644 Default.sublime-commands create mode 100644 Main.sublime-menu create mode 100644 Sublime Modelines.sublime-settings create mode 100644 app/__init__.py create mode 100644 app/settings.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..cc1923a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/Default.sublime-commands b/Default.sublime-commands new file mode 100644 index 0000000..f00a71b --- /dev/null +++ b/Default.sublime-commands @@ -0,0 +1,10 @@ +[ + { + "caption": "Preferences: Sublime Modelines Settings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/Modelines/Sublime Modelines.sublime-settings", + "default": "/* See the left pane for the list of settings and valid values. */\n{\n\t$0\n}\n", + } + } +] diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..aa6ec7a --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,22 @@ +[{ + "id": "preferences", + "children": [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": [ + { + "caption": "Sublime Modelines", + "id": "sublime-modelines-settings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/Modelines/Sublime Modelines.sublime-settings", + "default": "/* See the left pane for the list of settings and valid values. */\n{\n\t$0\n}\n", + } + } + ] + } + ] +} +] diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings new file mode 100644 index 0000000..3f94315 --- /dev/null +++ b/Sublime Modelines.sublime-settings @@ -0,0 +1,13 @@ +{ + /* Which types of modelines format are allowed. */ + "formats": [ + /* `# sublime: key val; ...` + * Usually works well unless putting the modeline in a `/*`-style comment. + * Can also not work when the syntax of the file is not known, because we check the line to begin with the comment char before parsing it. */ + "classic", + /* `#~~ sublime: key val; ... ~~` + * For this format the comment char does not matter. + * All that matter is there is a `~~` token before the 9th char of the line (and another one anywhere in the line). */ + "delimited", + ], +} diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..cef7b4b --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,9 @@ +from . import logger +from . import settings +from . import sublime_modelines + +__all__ = [ + "logger", + "settings", + "sublime_modelines", +] diff --git a/app/logger.py b/app/logger.py index e88c2bc..81bc76b 100644 --- a/app/logger.py +++ b/app/logger.py @@ -9,22 +9,30 @@ class Logger: log_to_tmp = False enable_debug_log = False - def __init__(self): - super().__init__() + def __new__(cls, *args, **kwargs): + raise RuntimeError("Logger is static and thus cannot be instantiated.") - def debug(self, s, *args): - if not self.enable_debug_log: + @staticmethod + def debug(s, *args): + if not Logger.enable_debug_log: return - self._log(self._format(s, *args)) - - def info(self, s, *args): - self._log(self._format(s, *args)) + Logger._log(Logger._format("", s, *args)) + + @staticmethod + def info(s, *args): + Logger._log(Logger._format("", s, *args)) + + @staticmethod + def warning(s, *args): + Logger._log(Logger._format("*** ", s, *args)) - def _format(self, s, *args): - return "[SublimeModelines] " + (s % args) + "\n" + @staticmethod + def _format(prefix, s, *args): + return "[Sublime Modelines] " + prefix + (s % args) + "\n" - def _log(self, str): - if self.log_to_tmp: + @staticmethod + def _log(str): + if Logger.log_to_tmp: with open("/tmp/sublime_modelines_debug.log", "a") as myfile: myfile.write(str) sys.stderr.write(str) diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..d48b7a4 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,43 @@ +from enum import Enum +import sublime + +from .logger import Logger + + + +class ModelineFormat(str, Enum): + CLASSIC = "classic" + DELIMITED = "delimited" + + +class Settings: + """ + A class that gives convenient access to the settings for our plugin. + + Creating an instance of this class will load the settings. + """ + + def __init__(self): + super().__init__() + self.settings = sublime.load_settings("Sublime Modelines.sublime-settings") + + def modelines_formats(self): + default_for_syntax_error = [ModelineFormat.CLASSIC] + + raw_formats = self.settings.get("formats") + if not isinstance(raw_formats, list): + Logger.warning("Did not get an array in the settings for the “formats” key.") + return default_for_syntax_error + + formats = [] + for raw_format in raw_formats: + if not isinstance(raw_format, str): + Logger.warning("Found an invalid value (not a string) in the “formats” key. Returning the default modeline formats.") + return default_for_syntax_error + + try: + formats.append(ModelineFormat(raw_format)) + except ValueError: + Logger.warning("Found an invalid value (unknown format) in the “formats” key. Skipping this value.") + + return formats diff --git a/app/sublime_modelines.py b/app/sublime_modelines.py index 8348725..6267e36 100644 --- a/app/sublime_modelines.py +++ b/app/sublime_modelines.py @@ -1,242 +1,242 @@ -import re, sys, json, os +# import re, sys, json, os -debug_log("Modelines plugin start.") +# debug_log("Modelines plugin start.") -MODELINE_PREFIX_TPL = "%s\\s*(st|sublime):" +# MODELINE_PREFIX_TPL = "%s\\s*(st|sublime):" -MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime):\x20?set\x20(.*):.*$") -MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime):(.*):.*$") +# MODELINE_TYPE_1 = re.compile(r"[\x20\t](st|sublime):\x20?set\x20(.*):.*$") +# MODELINE_TYPE_2 = re.compile(r"[\x20\t](st|sublime):(.*):.*$") -KEY_VALUE = re.compile(r"""(?x) \s* - (?P\w+) \s* (?P\+?=) \s* (?P - (?: "(?:\\.|[^"\\])*" - | [\[\{].* - | [^\s:]+ - )) - """) +# KEY_VALUE = re.compile(r"""(?x) \s* +# (?P\w+) \s* (?P\+?=) \s* (?P +# (?: "(?:\\.|[^"\\])*" +# | [\[\{].* +# | [^\s:]+ +# )) +# """) -KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") +# KEY_ONLY = re.compile(r"""(?x)\s*(?P\w+)""") -DEFAULT_LINE_COMMENT = "#" -MULTIOPT_SEP = "; " -MAX_LINES_TO_CHECK = 50 -LINE_LENGTH = 80 -MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH +# DEFAULT_LINE_COMMENT = "#" +# MULTIOPT_SEP = "; " +# MAX_LINES_TO_CHECK = 50 +# LINE_LENGTH = 80 +# MODELINES_REG_SIZE = MAX_LINES_TO_CHECK * LINE_LENGTH -ST3 = sublime.version() >= "3000" +# ST3 = sublime.version() >= "3000" -if ST3: - basestring = str +# if ST3: +# basestring = str -def get_output_panel(name): - if ST3: return sublime.active_window().create_output_panel(name) - else: return sublime.active_window().get_output_panel(name) +# def get_output_panel(name): +# if ST3: return sublime.active_window().create_output_panel(name) +# else: return sublime.active_window().get_output_panel(name) -def is_modeline(prefix, line): - return bool(re.match(prefix, line)) +# def is_modeline(prefix, line): +# return bool(re.match(prefix, line)) -def gen_modelines(view): - topRegEnd = min(MODELINES_REG_SIZE, view.size()) - candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) +# def gen_modelines(view): +# topRegEnd = min(MODELINES_REG_SIZE, view.size()) +# candidates = view.lines(sublime.Region(0, view.full_line(topRegEnd).end())) - # Consider modelines at the end of the buffer too. - # There might be overlap with the top region, but it doesn’t matter because it means the buffer is tiny. - bottomRegStart = view.size() - MODELINES_REG_SIZE - if bottomRegStart < 0: bottomRegStart = 0 +# # Consider modelines at the end of the buffer too. +# # There might be overlap with the top region, but it doesn’t matter because it means the buffer is tiny. +# bottomRegStart = view.size() - MODELINES_REG_SIZE +# if bottomRegStart < 0: bottomRegStart = 0 - candidates += view.lines(sublime.Region(bottomRegStart, view.size())) +# candidates += view.lines(sublime.Region(bottomRegStart, view.size())) - prefix = build_modeline_prefix(view) - modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) +# prefix = build_modeline_prefix(view) +# modelines = (view.substr(c) for c in candidates if is_modeline(prefix, view.substr(c))) - for modeline in modelines: - yield modeline +# for modeline in modelines: +# yield modeline -def gen_raw_options(modelines): - #import spdb ; spdb.start() - for m in modelines: - match = MODELINE_TYPE_1.search(m) - if not match: - match = MODELINE_TYPE_2.search(m) +# def gen_raw_options(modelines): +# #import spdb ; spdb.start() +# for m in modelines: +# match = MODELINE_TYPE_1.search(m) +# if not match: +# match = MODELINE_TYPE_2.search(m) - if match: - type, s = match.groups() +# if match: +# type, s = match.groups() - while True: - if s.startswith(":"): s = s[1:] +# while True: +# if s.startswith(":"): s = s[1:] - m = KEY_VALUE.match(s) - if m: - yield m.groups() - s = s[m.end():] - continue +# m = KEY_VALUE.match(s) +# if m: +# yield m.groups() +# s = s[m.end():] +# continue - m = KEY_ONLY.match(s) - if m: - k, = m.groups() - value = "true" +# m = KEY_ONLY.match(s) +# if m: +# k, = m.groups() +# value = "true" - yield k, "=", value +# yield k, "=", value - s = s[m.end():] - continue +# s = s[m.end():] +# continue - break +# break - continue +# continue - # Original sublime modelines style. - opt = m.partition(":")[2].strip() - if MULTIOPT_SEP in opt: - for subopt in (s for s in opt.split(MULTIOPT_SEP)): - yield subopt - else: - yield opt - - -def gen_modeline_options(view): - modelines = gen_modelines(view) - for opt in gen_raw_options(modelines): - if not isinstance(opt, tuple): - #import spdb ; spdb.start() - name, sep, value = opt.partition(" ") - yield view.settings().set, name.rstrip(":"), value.rstrip(";") +# # Original sublime modelines style. +# opt = m.partition(":")[2].strip() +# if MULTIOPT_SEP in opt: +# for subopt in (s for s in opt.split(MULTIOPT_SEP)): +# yield subopt +# else: +# yield opt + + +# def gen_modeline_options(view): +# modelines = gen_modelines(view) +# for opt in gen_raw_options(modelines): +# if not isinstance(opt, tuple): +# #import spdb ; spdb.start() +# name, sep, value = opt.partition(" ") +# yield view.settings().set, name.rstrip(":"), value.rstrip(";") - else: - name, op, value = opt +# else: +# name, op, value = opt - def _setter(n,v): - if op == "+=": - if v.startswith("{"): - default = {} - elif v.startswith("["): - default = [] - elif isinstance(v, basestring): - default = "" - else: - default = 0 +# def _setter(n,v): +# if op == "+=": +# if v.startswith("{"): +# default = {} +# elif v.startswith("["): +# default = [] +# elif isinstance(v, basestring): +# default = "" +# else: +# default = 0 - ov = view.settings().get(n, default) - v = ov + v +# ov = view.settings().get(n, default) +# v = ov + v - view.settings().set(n,v) +# view.settings().set(n,v) - yield _setter, name, value +# yield _setter, name, value -def build_modeline_prefix(view): - return (MODELINE_PREFIX_TPL % DEFAULT_LINE_COMMENT) +# def build_modeline_prefix(view): +# return (MODELINE_PREFIX_TPL % DEFAULT_LINE_COMMENT) -def to_json_type(v): - """Convert string value to proper JSON type.""" - if not isinstance(v, str): - return json.loads(json.dumps(v)) +# def to_json_type(v): +# """Convert string value to proper JSON type.""" +# if not isinstance(v, str): +# return json.loads(json.dumps(v)) - try: - return json.loads(v.strip()) - except Exception as e: - if v: - if v[0] not in "[{": - return v - raise ValueError("Could not convert from JSON: %s" % v) - - -class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): - """This plugin provides a feature similar to vim modelines. - Modelines set options local to the view by declaring them in the source code file itself. +# try: +# return json.loads(v.strip()) +# except Exception as e: +# if v: +# if v[0] not in "[{": +# return v +# raise ValueError("Could not convert from JSON: %s" % v) + + +# class ExecuteSublimeTextModeLinesCommand(sublime_plugin.EventListener): +# """This plugin provides a feature similar to vim modelines. +# Modelines set options local to the view by declaring them in the source code file itself. - Example: - mysourcecodefile.py - # sublime: gutter false - # sublime: translate_tab_to_spaces true +# Example: +# mysourcecodefile.py +# # sublime: gutter false +# # sublime: translate_tab_to_spaces true - The top as well as the bottom of the buffer is scanned for modelines. - MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be scanned. - """ +# The top as well as the bottom of the buffer is scanned for modelines. +# MAX_LINES_TO_CHECK * LINE_LENGTH defines the size of the regions to be scanned. +# """ - settings = None +# settings = None - def __init__(self): - self._modes = {} +# def __init__(self): +# self._modes = {} - def do_modelines(self, view): - if not self._modes: - self.init_syntax_files() +# def do_modelines(self, view): +# if not self._modes: +# self.init_syntax_files() - settings = view.settings() +# settings = view.settings() - ignored_packages = settings.get("ignored_packages") +# ignored_packages = settings.get("ignored_packages") - keys = set(settings.get("sublime_modelines_keys", [])) - new_keys = set() +# keys = set(settings.get("sublime_modelines_keys", [])) +# new_keys = set() - base_dir = settings.get("result_base_dir") +# base_dir = settings.get("result_base_dir") - for setter, name, value in gen_modeline_options(view): - debug_log("modeline: %s = %s", name, value) +# for setter, name, value in gen_modeline_options(view): +# debug_log("modeline: %s = %s", name, value) - if name == "x_syntax": - syntax_file = None - if value.lower() in self._modes: syntax_file = self._modes[value.lower()] - else: syntax_file = value +# if name == "x_syntax": +# syntax_file = None +# if value.lower() in self._modes: syntax_file = self._modes[value.lower()] +# else: syntax_file = value - if ST3: view.assign_syntax(syntax_file) - else: view.set_syntax_file(syntax_file) +# if ST3: view.assign_syntax(syntax_file) +# else: view.set_syntax_file(syntax_file) - new_keys.add("x_syntax") - debug_log("set syntax = %s" % syntax_file) +# new_keys.add("x_syntax") +# debug_log("set syntax = %s" % syntax_file) - else: - try: - setter(name, to_json_type(value)) - new_keys.add(name) - except ValueError as e: - sublime.status_message("[SublimeModelines] Bad modeline detected.") - log_to_console("Bad option detected: %s, %s.", name, value) - log_to_console("Tip: Keys cannot be empty strings.") +# else: +# try: +# setter(name, to_json_type(value)) +# new_keys.add(name) +# except ValueError as e: +# sublime.status_message("[SublimeModelines] Bad modeline detected.") +# log_to_console("Bad option detected: %s, %s.", name, value) +# log_to_console("Tip: Keys cannot be empty strings.") - for k in keys: - if k not in new_keys: - if settings.has(k): - settings.erase(k) +# for k in keys: +# if k not in new_keys: +# if settings.has(k): +# settings.erase(k) - settings.set("sublime_modelines_keys", list(new_keys)) +# settings.set("sublime_modelines_keys", list(new_keys)) - # From . - def init_syntax_files(self): - for syntax_file in self.find_syntax_files(): - name = os.path.splitext(os.path.basename(syntax_file))[0].lower() - self._modes[name] = syntax_file +# # From . +# def init_syntax_files(self): +# for syntax_file in self.find_syntax_files(): +# name = os.path.splitext(os.path.basename(syntax_file))[0].lower() +# self._modes[name] = syntax_file - # Load custom mappings from the settings file. - self.settings = sublime.load_settings("SublimeModelines.sublime-settings") +# # Load custom mappings from the settings file. +# self.settings = sublime.load_settings("SublimeModelines.sublime-settings") - if self.settings.has("mode_mappings"): - for modeline, syntax in self.settings.get("mode_mappings").items(): - self._modes[modeline] = self._modes[syntax.lower()] +# if self.settings.has("mode_mappings"): +# for modeline, syntax in self.settings.get("mode_mappings").items(): +# self._modes[modeline] = self._modes[syntax.lower()] - if self.settings.has("user_mode_mappings"): - for modeline, syntax in self.settings.get("user_mode_mappings").items(): - self._modes[modeline] = self._modes[syntax.lower()] +# if self.settings.has("user_mode_mappings"): +# for modeline, syntax in self.settings.get("user_mode_mappings").items(): +# self._modes[modeline] = self._modes[syntax.lower()] - # From . - def find_syntax_files(self): - # ST3 - if hasattr(sublime, "find_resources"): - for f in sublime.find_resources("*.tmLanguage"): - yield f - for f in sublime.find_resources("*.sublime-syntax"): - yield f - else: - for root, dirs, files in os.walk(sublime.packages_path()): - for f in files: - if f.endswith(".tmLanguage") or f.endswith("*.sublime-syntax"): - langfile = os.path.relpath(os.path.join(root, f), sublime.packages_path()) - # ST2 (as of build 2181) requires unix/MSYS style paths for the “syntax” view setting. - yield os.path.join("Packages", langfile).replace("\\", "/") +# # From . +# def find_syntax_files(self): +# # ST3 +# if hasattr(sublime, "find_resources"): +# for f in sublime.find_resources("*.tmLanguage"): +# yield f +# for f in sublime.find_resources("*.sublime-syntax"): +# yield f +# else: +# for root, dirs, files in os.walk(sublime.packages_path()): +# for f in files: +# if f.endswith(".tmLanguage") or f.endswith("*.sublime-syntax"): +# langfile = os.path.relpath(os.path.join(root, f), sublime.packages_path()) +# # ST2 (as of build 2181) requires unix/MSYS style paths for the “syntax” view setting. +# yield os.path.join("Packages", langfile).replace("\\", "/") diff --git a/plugin.py b/plugin.py index 95da120..7c96df5 100644 --- a/plugin.py +++ b/plugin.py @@ -1,5 +1,10 @@ +from importlib import reload import sublime, sublime_plugin + +from . import app + from .app.logger import Logger +from .app.settings import Settings @@ -46,18 +51,19 @@ class SublimeModelinesPlugin(sublime_plugin.EventListener): -> `["key1": "hello;semicolon and"]` """ - logger = Logger() - def __init__(self): super().__init__() - self.logger.log_to_tmp = True - self.logger.enable_debug_log = True - self.logger.debug("Plugin init.") + Logger.log_to_tmp = False + Logger.enable_debug_log = True + Logger.debug("Plugin init.") + Logger.debug("%s", Settings().modelines_formats()) def on_load(self, view): - self.logger.debug("on_load called.") + pass + Logger.debug("on_load called.") #self.do_modelines(view) def on_post_save(self, view): - self.logger.debug("on_post_save called.") + pass + Logger.debug("on_post_save called.") #self.do_modelines(view) From d35f3aec42c714e831fdbce5f5805a679ffe4cf8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 04:42:10 +0100 Subject: [PATCH 055/128] Remove the delimited mode from default formats --- Sublime Modelines.sublime-settings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 3f94315..e2c297e 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -1,13 +1,13 @@ { /* Which types of modelines format are allowed. */ "formats": [ - /* `# sublime: key val; ...` + /* `# sublime: key val(; key2 val2)*` * Usually works well unless putting the modeline in a `/*`-style comment. * Can also not work when the syntax of the file is not known, because we check the line to begin with the comment char before parsing it. */ "classic", - /* `#~~ sublime: key val; ... ~~` + /* `#~~ sublime: key val(; key2 val2)* ~~` * For this format the comment char does not matter. * All that matter is there is a `~~` token before the 9th char of the line (and another one anywhere in the line). */ - "delimited", + //"delimited", ], } From 44f875a5b51a86ccdbd5888b3a33869581f56ffd Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 04:43:05 +0100 Subject: [PATCH 056/128] Remove a stray log --- plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin.py b/plugin.py index 7c96df5..9346405 100644 --- a/plugin.py +++ b/plugin.py @@ -56,7 +56,6 @@ def __init__(self): Logger.log_to_tmp = False Logger.enable_debug_log = True Logger.debug("Plugin init.") - Logger.debug("%s", Settings().modelines_formats()) def on_load(self, view): pass From 99e6ab3ddf1756b8e7fa883aa2f61223fb09a0ff Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 04:43:53 +0100 Subject: [PATCH 057/128] Remove a useless import --- plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin.py b/plugin.py index 9346405..28be97a 100644 --- a/plugin.py +++ b/plugin.py @@ -1,8 +1,6 @@ from importlib import reload import sublime, sublime_plugin -from . import app - from .app.logger import Logger from .app.settings import Settings From 285b2a6a42584e911e332c48e785e60b82cda438 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 04:50:10 +0100 Subject: [PATCH 058/128] Add new setting to set the number of lines to scan to find the modelines --- Sublime Modelines.sublime-settings | 4 ++++ app/settings.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index e2c297e..39d49eb 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -10,4 +10,8 @@ * All that matter is there is a `~~` token before the 9th char of the line (and another one anywhere in the line). */ //"delimited", ], + + /* These two settings determine how far the search for a modeline should be done. */ + "number_of_lines_to_check_from_beginning": 5, + "number_of_lines_to_check_from_end": 5, } diff --git a/app/settings.py b/app/settings.py index d48b7a4..9b55c6e 100644 --- a/app/settings.py +++ b/app/settings.py @@ -41,3 +41,17 @@ def modelines_formats(self): Logger.warning("Found an invalid value (unknown format) in the “formats” key. Skipping this value.") return formats + + def number_of_lines_to_check_from_beginning(self): + raw_value = self.settings.get("number_of_lines_to_check_from_beginning") + if not isinstance(raw_value, int): + Logger.warning("Did not get an int in the settings for the number_of_lines_to_check_from_beginning key.") + return 5 + return raw_value + + def number_of_lines_to_check_from_end(self): + raw_value = self.settings.get("number_of_lines_to_check_from_end") + if not isinstance(raw_value, int): + Logger.warning("Did not get an int in the settings for the number_of_lines_to_check_from_end key.") + return 5 + return raw_value From 496431e2e0902065c8553f2e216ca2added7e963 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 12:46:41 +0100 Subject: [PATCH 059/128] Remove an unused import --- plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin.py b/plugin.py index 28be97a..5acc5a3 100644 --- a/plugin.py +++ b/plugin.py @@ -1,4 +1,3 @@ -from importlib import reload import sublime, sublime_plugin from .app.logger import Logger From e0a7863c20429613ecf64cf7eb3b7db7d2be8fbd Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 12:47:17 +0100 Subject: [PATCH 060/128] Add a pyrightconfig file for proper detection of sublime modules --- pyrightconfig.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pyrightconfig.json diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..5dacef8 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,8 @@ +{ + /* Install LSP-json to get validation and auto-completion in this file. */ + "venvPath": ".", + "venv": "sublime-modelines", + "extraPaths": [ + "/Applications/Sublime Text.app/Contents/MacOS/Lib/python38", + ] +} From 18d1e03ecb430c08c80667d704aefadac7c2b1d8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 12:50:41 +0100 Subject: [PATCH 061/128] Do not enable debug logs --- plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 5acc5a3..dcda269 100644 --- a/plugin.py +++ b/plugin.py @@ -51,7 +51,7 @@ class SublimeModelinesPlugin(sublime_plugin.EventListener): def __init__(self): super().__init__() Logger.log_to_tmp = False - Logger.enable_debug_log = True + Logger.enable_debug_log = False Logger.debug("Plugin init.") def on_load(self, view): From 96fc4fd9f38f683451907a721bc7f86ec4768c02 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 12:57:54 +0100 Subject: [PATCH 062/128] Remove useless passes --- plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin.py b/plugin.py index dcda269..576bfe9 100644 --- a/plugin.py +++ b/plugin.py @@ -55,11 +55,9 @@ def __init__(self): Logger.debug("Plugin init.") def on_load(self, view): - pass Logger.debug("on_load called.") #self.do_modelines(view) def on_post_save(self, view): - pass Logger.debug("on_post_save called.") #self.do_modelines(view) From 2dce7c62774870715ef83c8c780d5d5c3d86b2ee Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 13:06:27 +0100 Subject: [PATCH 063/128] Add type hints in the code --- app/logger.py | 10 +++++----- app/settings.py | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/logger.py b/app/logger.py index 81bc76b..48e46d1 100644 --- a/app/logger.py +++ b/app/logger.py @@ -13,25 +13,25 @@ def __new__(cls, *args, **kwargs): raise RuntimeError("Logger is static and thus cannot be instantiated.") @staticmethod - def debug(s, *args): + def debug(s: str, *args) -> None: if not Logger.enable_debug_log: return Logger._log(Logger._format("", s, *args)) @staticmethod - def info(s, *args): + def info(s: str, *args) -> None: Logger._log(Logger._format("", s, *args)) @staticmethod - def warning(s, *args): + def warning(s: str, *args) -> None: Logger._log(Logger._format("*** ", s, *args)) @staticmethod - def _format(prefix, s, *args): + def _format(prefix: str, s: str, *args) -> str: return "[Sublime Modelines] " + prefix + (s % args) + "\n" @staticmethod - def _log(str): + def _log(str: str) -> None: if Logger.log_to_tmp: with open("/tmp/sublime_modelines_debug.log", "a") as myfile: myfile.write(str) diff --git a/app/settings.py b/app/settings.py index 9b55c6e..e3223f4 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,3 +1,6 @@ +# This can be removed when using Python >= 3.10. +from typing import List + from enum import Enum import sublime @@ -21,7 +24,7 @@ def __init__(self): super().__init__() self.settings = sublime.load_settings("Sublime Modelines.sublime-settings") - def modelines_formats(self): + def modelines_formats(self) -> List[ModelineFormat]: default_for_syntax_error = [ModelineFormat.CLASSIC] raw_formats = self.settings.get("formats") @@ -42,14 +45,14 @@ def modelines_formats(self): return formats - def number_of_lines_to_check_from_beginning(self): + def number_of_lines_to_check_from_beginning(self) -> int: raw_value = self.settings.get("number_of_lines_to_check_from_beginning") if not isinstance(raw_value, int): Logger.warning("Did not get an int in the settings for the number_of_lines_to_check_from_beginning key.") return 5 return raw_value - def number_of_lines_to_check_from_end(self): + def number_of_lines_to_check_from_end(self) -> int: raw_value = self.settings.get("number_of_lines_to_check_from_end") if not isinstance(raw_value, int): Logger.warning("Did not get an int in the settings for the number_of_lines_to_check_from_end key.") From 6699c2b6630f336c2de3fafbab2db5bf49f40a46 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 26 Jan 2026 16:02:57 +0100 Subject: [PATCH 064/128] Add more info about the formats key of the preferences --- Sublime Modelines.sublime-settings | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 39d49eb..b22f015 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -1,9 +1,12 @@ { - /* Which types of modelines format are allowed. */ + /* Which types of modelines format are allowed. + * When multiple formats are specified, the parsing is attempted using the formats in the order they are given. */ "formats": [ /* `# sublime: key val(; key2 val2)*` * Usually works well unless putting the modeline in a `/*`-style comment. - * Can also not work when the syntax of the file is not known, because we check the line to begin with the comment char before parsing it. */ + * Can also not work when the syntax of the file is not known, + * because we check the line to begin with the comment char before parsing it + * (`#` is used when the character is unknown). */ "classic", /* `#~~ sublime: key val(; key2 val2)* ~~` * For this format the comment char does not matter. From e7dde30b96e7b6180153ec8f96b018a9e72d2ff2 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 27 Jan 2026 10:51:35 +0100 Subject: [PATCH 065/128] Add more info about the log_to_tmp variable in the logger --- app/logger.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/logger.py b/app/logger.py index 48e46d1..a11e793 100644 --- a/app/logger.py +++ b/app/logger.py @@ -6,8 +6,11 @@ class Logger: """A simple logger.""" # Default config for the logger. - log_to_tmp = False + # Regarding the logging to a tmp file, this dates back to a time where I did not know how to show the console in Sublime (ctrl-`). + # I used to log to a temporary file that I tailed. + # Now this should probably always be False. enable_debug_log = False + log_to_tmp = False def __new__(cls, *args, **kwargs): raise RuntimeError("Logger is static and thus cannot be instantiated.") From b6f097ff39b68c4a8f4d03a3da61a88da2a4618c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 27 Jan 2026 14:04:33 +0100 Subject: [PATCH 066/128] Empty the __init__ file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently everything works with an empty __init__… --- app/__init__.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index cef7b4b..e69de29 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,9 +0,0 @@ -from . import logger -from . import settings -from . import sublime_modelines - -__all__ = [ - "logger", - "settings", - "sublime_modelines", -] From 566d0c0d50a3379ea62a3dddfa4d381411806501 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 27 Jan 2026 14:05:40 +0100 Subject: [PATCH 067/128] Load Logger config from settings --- Sublime Modelines.sublime-settings | 8 ++++++++ app/logger.py | 16 ++++++++++------ app/settings.py | 14 ++++++++++++++ plugin.py | 3 +-- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index b22f015..324c6f2 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -17,4 +17,12 @@ /* These two settings determine how far the search for a modeline should be done. */ "number_of_lines_to_check_from_beginning": 5, "number_of_lines_to_check_from_end": 5, + + /* Whether debug logs should be enabled. */ + "verbose": false, + /* Whether to log to `/tmp/sublime_modelines_debug.log` in addition to stderr. + * This dates back to a time where I did not know how to show the console in Sublime (ctrl-`). + * I used to log to a temporary file that I tailed. + * Now this should probably always be False. */ + "log_to_tmp": false, } diff --git a/app/logger.py b/app/logger.py index a11e793..32ea42b 100644 --- a/app/logger.py +++ b/app/logger.py @@ -1,19 +1,20 @@ import sys +from .settings import Settings + class Logger: """A simple logger.""" - # Default config for the logger. - # Regarding the logging to a tmp file, this dates back to a time where I did not know how to show the console in Sublime (ctrl-`). - # I used to log to a temporary file that I tailed. - # Now this should probably always be False. enable_debug_log = False log_to_tmp = False - def __new__(cls, *args, **kwargs): - raise RuntimeError("Logger is static and thus cannot be instantiated.") + @staticmethod + def updateSettings() -> None: + settings = Settings() + Logger.enable_debug_log = settings.verbose() + Logger.log_to_tmp = settings.log_to_tmp() @staticmethod def debug(s: str, *args) -> None: @@ -39,3 +40,6 @@ def _log(str: str) -> None: with open("/tmp/sublime_modelines_debug.log", "a") as myfile: myfile.write(str) sys.stderr.write(str) + + def __new__(cls, *args, **kwargs): + raise RuntimeError("Logger is static and thus cannot be instantiated.") diff --git a/app/settings.py b/app/settings.py index e3223f4..3b7256a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -58,3 +58,17 @@ def number_of_lines_to_check_from_end(self) -> int: Logger.warning("Did not get an int in the settings for the number_of_lines_to_check_from_end key.") return 5 return raw_value + + def verbose(self) -> bool: + raw_value = self.settings.get("verbose") + if not isinstance(raw_value, bool): + Logger.warning("Did not get a bool in the settings for the verbose key.") + return False + return raw_value + + def log_to_tmp(self) -> bool: + raw_value = self.settings.get("log_to_tmp") + if not isinstance(raw_value, bool): + Logger.warning("Did not get a bool in the settings for the log_to_tmp key.") + return False + return raw_value diff --git a/plugin.py b/plugin.py index 576bfe9..0d84f33 100644 --- a/plugin.py +++ b/plugin.py @@ -50,8 +50,7 @@ class SublimeModelinesPlugin(sublime_plugin.EventListener): def __init__(self): super().__init__() - Logger.log_to_tmp = False - Logger.enable_debug_log = False + Logger.updateSettings() Logger.debug("Plugin init.") def on_load(self, view): From b85eb27024628e17dc01cff6773c6cbaa7018f9c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 27 Jan 2026 14:06:11 +0100 Subject: [PATCH 068/128] =?UTF-8?q?Add=20all=20the=20=E2=80=9Cspecs?= =?UTF-8?q?=E2=80=9D=20for=20the=20project=20in=20the=20settings=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make a comment more precise --- Sublime Modelines.sublime-settings | 179 +++++++++++++++++++++++++++-- app/settings.py | 8 +- plugin.py | 41 ------- 3 files changed, 174 insertions(+), 54 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 324c6f2..784696b 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -1,22 +1,180 @@ { + + /* These two settings determine how far the search for a modeline should be done. */ + "number_of_lines_to_check_from_beginning": 5, + "number_of_lines_to_check_from_end": 5, + /* Which types of modelines format are allowed. * When multiple formats are specified, the parsing is attempted using the formats in the order they are given. */ "formats": [ - /* `# sublime: key val(; key2 val2)*` + + /* Default format. + * Examples: + * `// ~*~ sublime: key=val; key2=val2 ~*~` + * `// ~*~ sublime: key = val; key2+=val2; ~*~` + * `// ~*~ sublime: key=["hello": "world"] ~*~` + * + * This format is inspired by VIM (`sublime:` prefix, key=val) as well as Emacs (`~*~` delimiters; Emacs uses `-*-`). + * + * Any value that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) is parsed as a JSON string. + * The literal strings `true` and `false` are converted to their boolean values. + * Literal numbers (`42`, `3.14`, `-007`, `+12.345`) are converted to numbers. + * The literal string `null` is converted to None. + * You can use double-quotes for these cases if you need an explicit string instead. + * + * All values are trimmed of their spaces (before being parsed if the value is a JSON string). + * If a value should contain a semicolon (`;`), it should be doubled (included inside a JSON string) + * to avoid being interpreted as the delimiter for the end of the value. */ + "default", + + /* Classic (legacy) format. + * Example: `# sublime: key val(; key2 val2)*` + * * Usually works well unless putting the modeline in a `/*`-style comment. + * * Can also not work when the syntax of the file is not known, * because we check the line to begin with the comment char before parsing it - * (`#` is used when the character is unknown). */ - "classic", - /* `#~~ sublime: key val(; key2 val2)* ~~` - * For this format the comment char does not matter. - * All that matter is there is a `~~` token before the 9th char of the line (and another one anywhere in the line). */ - //"delimited", + * (`#` is used when the character is unknown). + * + * The parsing algorithm is exactly the same as the old ST2 version of the plugin. */ + //"classic", + + /* VIM-like modelines. + * Examples (straight from ): + * - `// vim: noai:ts=4:sw=4` + * - `/* vim: noai:ts=4:sw=4` (closing comment token is on next line) */ + // - `/* vim: set noai ts=4 sw=4: */` + // - `/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */` + /* + * For this format we map the VIM commands to Sublime Text commands. + * Additional mapping can be added in this config file. + * + * It is also possible to prefix commands with `st-`, `sublime-`, `sublime-text-` or `sublimetext-` + * to directly execute Sublime Text commands without needing any mapping. + * + * See the Readme for more information. */ + //"vim", + + /* Emacs-like modelines. + * Examples: + * `-*- key: value; key2: value2 -*-` + * `-*- syntax_name -*-` + * + * Just like for the VIM format, we map the Emacs commands to Sublime Text commands. */ + //"emacs", + ], - /* These two settings determine how far the search for a modeline should be done. */ - "number_of_lines_to_check_from_beginning": 5, - "number_of_lines_to_check_from_end": 5, + /* Default VIM commands mapping. + * Use can use `vim_mapping_user` to define your own mapping while keeping this one. + * From . */ + "vim_mapping": { + /* Enable/disable automatic indentation. */ + "autoindent": {"aliases": ["ai"], "key": "auto_indent", "value": true}, + "noautoindent": {"aliases": ["noai"], "key": "auto_indent", "value": false}, + /* Set line endings (DOS, Legacy MacOS, UNIX). */ + "fileformat": {"aliases": ["ff"], "key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR"/* unix is not needed, the value is the same */}}, + /* Set the syntax of the file. */ + "filetype": {"aliases": ["ft"], "key": "x_syntax"}, + /* # of columns for each tab character. */ + "tabstop": {"aliases": ["ts"], "key": "tab_size"}, + /* # of columns for indent operation. */ + "shiftwidth": {"aliases": ["sw"], "key": null /* Not supported by Sublime. */}, + /* # of columns for tab key (space & tab). */ + "softtab": {"aliases": ["st"], "key": null /* Not supported by Sublime. */}, + /* Tabs → Spaces enable/disable. */ + "expandtab": {"aliases": ["et"], "key": "translate_tabs_to_spaces", "value": true}, + "noexpandtab": {"aliases": ["noet"], "key": "translate_tabs_to_spaces", "value": false}, + /* Show/hide line number. */ + "number": {"aliases": ["nu"], "key": "line_numbers", "value": true}, + "nonumber": {"aliases": ["nonu"], "key": "line_numbers", "value": false}, + /* Enable/disable word wrap. */ + "wrap": {"key": "word_wrap", "value": true}, + "nowrap": {"key": "word_wrap", "value": false}, + /* Set file encoding. */ + "fileencoding": {"aliases": ["fenc"], "key": "set_encoding()", "value-transforms": [ + {"type": "lowercase"}, + {"type": "map", "parameters": {"table": { + /* null values are unsupported and will set the status line for the plugin to notify of the failure. */ + "latin1": "Western (Windows 1252)", + "koi8-r": "Cyrillic (KOI8-R)", + "koi8-u": "Cyrillic (KOI8-U)", + "macroman": "Western (Mac Roman)", + "iso-8859-1": "Western (ISO 8859-1)", + "iso-8859-2": "Central European (ISO 8859-2)", + "iso-8859-3": "Western (ISO 8859-3)", + "iso-8859-4": "Baltic (ISO 8859-4)", + "iso-8859-5": "Cyrillic (ISO 8859-5)", + "iso-8859-6": "Arabic (ISO 8859-6)", + "iso-8859-7": "Greek (ISO 8859-7)", + "iso-8859-8": "Hebrew (ISO 8859-8)", + "iso-8859-9": "Turkish (ISO 8859-9)", + "iso-8859-10": "Nordic (ISO 8859-10)", + "iso-8859-13": "Estonian (ISO 8859-13)", + "iso-8859-14": "Celtic (ISO 8859-14)", + "iso-8859-15": "Western (ISO 8859-15)", + "iso-8859-16": "Romanian (ISO 8859-16)", + "cp437": "DOS (CP 437)", + "cp737": null, + "cp775": null, + "cp850": null, + "cp852": null, + "cp855": null, + "cp857": null, + "cp860": null, + "cp861": null, + "cp862": null, + "cp863": null, + "cp865": null, + "cp866": "Cyrillic (Windows 866)", + "cp869": null, + "cp874": null, + "cp1250": "Central European (Windows 1250)", + "cp1251": "Cyrillic (Windows 1251)", + "cp1252": "Western (Windows 1252)", + "cp1253": "Greek (Windows 1253)", + "cp1254": "Turkish (Windows 1254)", + "cp1255": "Hebrew (Windows 1255)", + "cp1256": "Arabic (Windows 1256)", + "cp1257": "Baltic (Windows 1257)", + "cp1258": "Vietnamese (Windows 1258)", + "cp932": null, + "euc-jp": null, + "sjis ": null, + "cp949": null, + "euc-kr": null, + "cp936": null, + "euc-cn": null, + "cp950": null, + "big5": null, + "euc-tw": null, + "utf-8": "utf-8", + "ucs-2le": "utf-16 le", + "utf-16": "utf-16 be", + "utf-16le": "utf-16 le", + "ucs-4": null, + "ucs-4le": null + }}}, + ]}, + }, + /* User mapping for VIM modelines. */ + "vim_mapping_user": {}, + + /* Default Emacs commands mapping. + * Use can use `emacs_mapping_user` to define your own mapping while keeping this one. + * From . */ + "emacs_mapping": { + /* Set line endings (DOS, Legacy MacOS, UNIX). */ + "coding": {"key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR"/* unix is not needed, the value is the same */}}, + /* Tabs → Spaces enable/disable. */ + "indent-tabs-mode": {"key": "translate_tabs_to_spaces", "value-mapping": {"nil": true, "0": true}, "value-mapping-default": false}, + /* Set the syntax of the file. */ + "mode": {"key": "x_syntax"}, + /* # of columns for each tab character. */ + "tab-width": {"key": "tab_size"}, + }, + /* User mapping for Emacs modelines. */ + "emacs_mapping_user": {}, /* Whether debug logs should be enabled. */ "verbose": false, @@ -25,4 +183,5 @@ * I used to log to a temporary file that I tailed. * Now this should probably always be False. */ "log_to_tmp": false, + } diff --git a/app/settings.py b/app/settings.py index 3b7256a..2ae85d0 100644 --- a/app/settings.py +++ b/app/settings.py @@ -9,8 +9,10 @@ class ModelineFormat(str, Enum): - CLASSIC = "classic" - DELIMITED = "delimited" + DEFAULT = "default" + LEGACY = "classic" + VIM = "vim" + EMACS = "emacs" class Settings: @@ -25,7 +27,7 @@ def __init__(self): self.settings = sublime.load_settings("Sublime Modelines.sublime-settings") def modelines_formats(self) -> List[ModelineFormat]: - default_for_syntax_error = [ModelineFormat.CLASSIC] + default_for_syntax_error = [ModelineFormat.DEFAULT] raw_formats = self.settings.get("formats") if not isinstance(raw_formats, list): diff --git a/plugin.py b/plugin.py index 0d84f33..726fdbd 100644 --- a/plugin.py +++ b/plugin.py @@ -6,47 +6,6 @@ class SublimeModelinesPlugin(sublime_plugin.EventListener): - """ - This plugin provides a feature similar to vim modelines, - which allow setting options local to the view by declaring them in the source code file itself. - - A special token is searched in the source code, which declares a modeline (see later for more info about the token). - - The top as well as the bottom of the buffer is scanned for modelines - (`MAX_LINES_TO_CHECK * LINE_LENGTH` defines the size of the regions to be scanned). - - For example, at the end or the beginning of a Python source file, one may find: - ```python - # sublime: gutter false; translate_tab_to_spaces true - ``` - - Token formats: - - - `^\\s*(sublime|st):\\s*key1\\s+val1(\\s*;\\s*keyn\\s+valn)\\s*;?` - - `.{1,7}~~\\s(sublime|st):\\s*key1\\s+val1(\\s*;\\s*keyn\\s+valn)\\s*;?\\s*~~` - - The first format works well if you do not change the syntax of the file. - If you do it is recommended to use the second format - (because the “comment char” is unknown and will thus default to `#`, which may not work for the syntax you need). - - The second format assumes the comment marker (beginning of the line) will have between 1 and 7 characters. - - Also the first format does not really work with `/**/`-style comments as the trailing `*/` will be parsed if it is on the same line as the `/*`. - - All the keys are guaranteed to never have any space, so there are never any ambiguities parsing them. - For the values, to have a semicolon inside, you can escape it by doubling it. - Having a space in the value is ok, except at the beginning or the end, because they will be trimmed. - (It is _not_ possible at all to have a value with one or more spaces at the beginning or the end.) - - When using the second format, values cannot contain a `~~` either. - - Examples: - - - `# sublime: key1 value1; key2 value with space ; key3 hello;;semicolon!;;; key4 last one;` - -> `["key1": "value1", "key2": "value with space", "key3": "hello;semicolon!;" "key4": "last one"]` - - `/*~~ sublime: key1 hello;;semicolon and~~tilde key2 is this parsed? */` - -> `["key1": "hello;semicolon and"]` - """ def __init__(self): super().__init__() From 28a0a4bdf457afe850e0e6f8be3ce7de63b4a08a Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 28 Jan 2026 03:45:23 +0100 Subject: [PATCH 069/128] Fix circular import --- app/logger+settings.py | 13 +++++++++++++ app/logger.py | 8 -------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 app/logger+settings.py diff --git a/app/logger+settings.py b/app/logger+settings.py new file mode 100644 index 0000000..8baebb4 --- /dev/null +++ b/app/logger+settings.py @@ -0,0 +1,13 @@ +import sys + +from .logger import Logger +from .settings import Settings + + + +def _updateLoggerSettings() -> None: + settings = Settings() + Logger.enable_debug_log = settings.verbose() + Logger.log_to_tmp = settings.log_to_tmp() + +Logger.updateSettings = _updateLoggerSettings diff --git a/app/logger.py b/app/logger.py index 32ea42b..4e2fae9 100644 --- a/app/logger.py +++ b/app/logger.py @@ -1,7 +1,5 @@ import sys -from .settings import Settings - class Logger: @@ -10,12 +8,6 @@ class Logger: enable_debug_log = False log_to_tmp = False - @staticmethod - def updateSettings() -> None: - settings = Settings() - Logger.enable_debug_log = settings.verbose() - Logger.log_to_tmp = settings.log_to_tmp() - @staticmethod def debug(s: str, *args) -> None: if not Logger.enable_debug_log: From d795375576233a98e24b420e4737a200f0fc4aea Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 29 Jan 2026 02:04:01 +0100 Subject: [PATCH 070/128] Fix plugin init --- app/logger.py | 1 + app/{logger+settings.py => logger_settings.py} | 5 ++--- plugin.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) rename app/{logger+settings.py => logger_settings.py} (54%) diff --git a/app/logger.py b/app/logger.py index 4e2fae9..22b60e9 100644 --- a/app/logger.py +++ b/app/logger.py @@ -2,6 +2,7 @@ +# Note: I tried logging with colors, it does not work (in the Sublime console). class Logger: """A simple logger.""" diff --git a/app/logger+settings.py b/app/logger_settings.py similarity index 54% rename from app/logger+settings.py rename to app/logger_settings.py index 8baebb4..cbbce62 100644 --- a/app/logger+settings.py +++ b/app/logger_settings.py @@ -5,9 +5,8 @@ -def _updateLoggerSettings() -> None: +# This cannot be defined in Logger because we need to import Settings to implement the function, and Settings uses Logger… +def updateLoggerSettings() -> None: settings = Settings() Logger.enable_debug_log = settings.verbose() Logger.log_to_tmp = settings.log_to_tmp() - -Logger.updateSettings = _updateLoggerSettings diff --git a/plugin.py b/plugin.py index 726fdbd..cc13bd8 100644 --- a/plugin.py +++ b/plugin.py @@ -2,6 +2,7 @@ from .app.logger import Logger from .app.settings import Settings +from .app.logger_settings import updateLoggerSettings @@ -9,7 +10,7 @@ class SublimeModelinesPlugin(sublime_plugin.EventListener): def __init__(self): super().__init__() - Logger.updateSettings() + updateLoggerSettings() Logger.debug("Plugin init.") def on_load(self, view): From 53f8d60a29c1409673d83f310390b09b71eb6a65 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 29 Jan 2026 02:06:11 +0100 Subject: [PATCH 071/128] Dummy typo fixes --- Sublime Modelines.sublime-settings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 784696b..063be59 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -163,7 +163,7 @@ /* Default Emacs commands mapping. * Use can use `emacs_mapping_user` to define your own mapping while keeping this one. * From . */ - "emacs_mapping": { + "emacs_mapping": { /* Set line endings (DOS, Legacy MacOS, UNIX). */ "coding": {"key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR"/* unix is not needed, the value is the same */}}, /* Tabs → Spaces enable/disable. */ @@ -174,14 +174,14 @@ "tab-width": {"key": "tab_size"}, }, /* User mapping for Emacs modelines. */ - "emacs_mapping_user": {}, + "emacs_mapping_user": {}, /* Whether debug logs should be enabled. */ "verbose": false, /* Whether to log to `/tmp/sublime_modelines_debug.log` in addition to stderr. * This dates back to a time where I did not know how to show the console in Sublime (ctrl-`). * I used to log to a temporary file that I tailed. - * Now this should probably always be False. */ + * Now this should probably always be `false`. */ "log_to_tmp": false, } From 14d1535b080cb4b9f23c6bdd239d645e4104a3ff Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 10 Feb 2026 01:04:38 +0100 Subject: [PATCH 072/128] Update plugin bootstrap structure --- plugin.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index cc13bd8..6d74c4a 100644 --- a/plugin.py +++ b/plugin.py @@ -1,3 +1,5 @@ +from typing import Final, Optional + import sublime, sublime_plugin from .app.logger import Logger @@ -5,18 +7,68 @@ from .app.logger_settings import updateLoggerSettings +# The plugin structure is heavily inspired by . +# We have mostly added typing, and fixed a potential issue if on_load or on_post_save is called in a view which is not the front-most one in a window. + + +PLUGIN_NAME: Final[str] = "SublimeModelines" + +# Before everything else, update the settings of the logger. +updateLoggerSettings() + + +def plugin_loaded(): + Logger.debug("Plugin loaded.") + + # Call on_load() for existing views, since files may load before the plugin. + # First we verify the plugin is properly instantiated (it should be). + plugin = SublimeModelinesPlugin.instance + if plugin is None: + Logger.warning("Plugin instance is not set.") + return + + for w in sublime.windows(): + for g in range(w.num_groups()): + view = w.active_view_in_group(g) + if view is None: continue + plugin.on_load(view) + + +def plugin_unloaded(): + Logger.debug("Plugin unloaded.") + class SublimeModelinesPlugin(sublime_plugin.EventListener): + """Event listener to invoke the command on load & save.""" + + instance: Optional[SublimeModelinesPlugin] = None def __init__(self): super().__init__() - updateLoggerSettings() - Logger.debug("Plugin init.") + Logger.debug("EventListener init.") + SublimeModelinesPlugin.instance = self - def on_load(self, view): + def on_load(self, view: sublime.View) -> None: Logger.debug("on_load called.") - #self.do_modelines(view) + do_modelines(view) - def on_post_save(self, view): + def on_post_save(self, view: sublime.View) -> None: Logger.debug("on_post_save called.") - #self.do_modelines(view) + do_modelines(view) + + +class SublimeModelinesApplyCommand(sublime_plugin.WindowCommand): + """Apply modelines in the given view.""" + + def run(self): + view = self.window.active_view() + if view is None or view.is_scratch(): + return + + do_modelines(view) + + +def do_modelines(view: sublime.View) -> None: + Logger.debug("Searching for and applying modelines.") + + view.erase_status(PLUGIN_NAME) From 8072663b91dd3dab4cb31304f7304047571b558c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 10 Feb 2026 10:49:31 +0100 Subject: [PATCH 073/128] Pass the settings object to updateLoggerSettings --- app/logger_settings.py | 3 +-- plugin.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/logger_settings.py b/app/logger_settings.py index cbbce62..33b364b 100644 --- a/app/logger_settings.py +++ b/app/logger_settings.py @@ -6,7 +6,6 @@ # This cannot be defined in Logger because we need to import Settings to implement the function, and Settings uses Logger… -def updateLoggerSettings() -> None: - settings = Settings() +def updateLoggerSettings(settings: Settings) -> None: Logger.enable_debug_log = settings.verbose() Logger.log_to_tmp = settings.log_to_tmp() diff --git a/plugin.py b/plugin.py index 6d74c4a..6421b8a 100644 --- a/plugin.py +++ b/plugin.py @@ -14,7 +14,8 @@ PLUGIN_NAME: Final[str] = "SublimeModelines" # Before everything else, update the settings of the logger. -updateLoggerSettings() +settings = Settings() +updateLoggerSettings(settings) def plugin_loaded(): From af13733b6a46e29a02948c2cc924dd5222bdf61f Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 10 Feb 2026 10:50:25 +0100 Subject: [PATCH 074/128] Add new settings to prevent apply on load and/or save --- Sublime Modelines.sublime-settings | 5 ++++- app/settings.py | 14 ++++++++++++++ plugin.py | 6 ++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 063be59..f7ac1da 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -1,4 +1,8 @@ { + /* Apply modelines on file open. */ + "apply_on_load": true, + /* Apply modelines on file save. */ + "apply_on_save": true, /* These two settings determine how far the search for a modeline should be done. */ "number_of_lines_to_check_from_beginning": 5, @@ -183,5 +187,4 @@ * I used to log to a temporary file that I tailed. * Now this should probably always be `false`. */ "log_to_tmp": false, - } diff --git a/app/settings.py b/app/settings.py index 2ae85d0..c677a18 100644 --- a/app/settings.py +++ b/app/settings.py @@ -47,6 +47,20 @@ def modelines_formats(self) -> List[ModelineFormat]: return formats + def apply_on_load(self) -> bool: + raw_value = self.settings.get("apply_on_load") + if not isinstance(raw_value, bool): + Logger.warning("Did not get a bool in the settings for the apply_on_load key.") + return False + return raw_value + + def apply_on_save(self) -> bool: + raw_value = self.settings.get("apply_on_save") + if not isinstance(raw_value, bool): + Logger.warning("Did not get a bool in the settings for the apply_on_save key.") + return False + return raw_value + def number_of_lines_to_check_from_beginning(self) -> int: raw_value = self.settings.get("number_of_lines_to_check_from_beginning") if not isinstance(raw_value, int): diff --git a/plugin.py b/plugin.py index 6421b8a..c28fc15 100644 --- a/plugin.py +++ b/plugin.py @@ -51,11 +51,13 @@ def __init__(self): def on_load(self, view: sublime.View) -> None: Logger.debug("on_load called.") - do_modelines(view) + if settings.apply_on_load: + do_modelines(view) def on_post_save(self, view: sublime.View) -> None: Logger.debug("on_post_save called.") - do_modelines(view) + if settings.apply_on_save: + do_modelines(view) class SublimeModelinesApplyCommand(sublime_plugin.WindowCommand): From cf9952ae03fc82092dc78adab1e14b2ae351bc29 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 10 Feb 2026 10:55:09 +0100 Subject: [PATCH 075/128] Add new command to apply modelines manually --- Default.sublime-commands | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Default.sublime-commands b/Default.sublime-commands index f00a71b..fe5a0ba 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -6,5 +6,9 @@ "base_file": "${packages}/Modelines/Sublime Modelines.sublime-settings", "default": "/* See the left pane for the list of settings and valid values. */\n{\n\t$0\n}\n", } - } + }, + { + "caption": "Sublime Modelines: Apply", + "command": "sublime_modelines_apply", + }, ] From cf3e1533cb083f017aa2379dacadb5503e8ec858 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Tue, 10 Feb 2026 22:05:25 +0100 Subject: [PATCH 076/128] Add a comment with the command name --- plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin.py b/plugin.py index c28fc15..41c55a8 100644 --- a/plugin.py +++ b/plugin.py @@ -60,6 +60,8 @@ def on_post_save(self, view: sublime.View) -> None: do_modelines(view) +# The command name will be `sublime_modelines_apply`. +# See [the rules to get command names]() for more info. class SublimeModelinesApplyCommand(sublime_plugin.WindowCommand): """Apply modelines in the given view.""" From 4b4cea0cd261b3e5c8ad2e6570808143c7bc48ce Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:13:09 +0200 Subject: [PATCH 077/128] Dummy comment/code-style fixes in the settings file --- Sublime Modelines.sublime-settings | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index f7ac1da..5a3d01f 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -11,13 +11,13 @@ /* Which types of modelines format are allowed. * When multiple formats are specified, the parsing is attempted using the formats in the order they are given. */ "formats": [ - /* Default format. * Examples: * `// ~*~ sublime: key=val; key2=val2 ~*~` * `// ~*~ sublime: key = val; key2+=val2; ~*~` * `// ~*~ sublime: key=["hello": "world"] ~*~` - * + * (Also works with /*-styled comments, but JSON does not supported nested comments, so I’m skipping this example…) + * * This format is inspired by VIM (`sublime:` prefix, key=val) as well as Emacs (`~*~` delimiters; Emacs uses `-*-`). * * Any value that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) is parsed as a JSON string. @@ -46,7 +46,7 @@ /* VIM-like modelines. * Examples (straight from ): * - `// vim: noai:ts=4:sw=4` - * - `/* vim: noai:ts=4:sw=4` (closing comment token is on next line) */ + * - `/* vim: noai:ts=4:sw=4` (w/ closing comment token on next line) */ // - `/* vim: set noai ts=4 sw=4: */` // - `/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */` /* @@ -66,7 +66,6 @@ * * Just like for the VIM format, we map the Emacs commands to Sublime Text commands. */ //"emacs", - ], /* Default VIM commands mapping. @@ -99,7 +98,8 @@ "fileencoding": {"aliases": ["fenc"], "key": "set_encoding()", "value-transforms": [ {"type": "lowercase"}, {"type": "map", "parameters": {"table": { - /* null values are unsupported and will set the status line for the plugin to notify of the failure. */ + /* null values are explicitly unsupported and will set the status line for the plugin to notify of the failure. + * If you use an encoding not in the list, it is implicitly unsupported and will also set the status line error. */ "latin1": "Western (Windows 1252)", "koi8-r": "Cyrillic (KOI8-R)", "koi8-u": "Cyrillic (KOI8-U)", From b14372b4d00505d56b7f7510c16cc8cdd42f4eb1 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:13:27 +0200 Subject: [PATCH 078/128] Fix a compilation error in main file --- plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 41c55a8..77a4d7e 100644 --- a/plugin.py +++ b/plugin.py @@ -42,7 +42,8 @@ def plugin_unloaded(): class SublimeModelinesPlugin(sublime_plugin.EventListener): """Event listener to invoke the command on load & save.""" - instance: Optional[SublimeModelinesPlugin] = None + #instance: Optional[SublimeModelinesPlugin] = None + instance = None def __init__(self): super().__init__() From c910f3417a8c1ea42d879903b6e27f263fdf79dc Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:13:49 +0200 Subject: [PATCH 079/128] Create a new Utils class for doing type casting --- app/utils.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/utils.py diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..1bfb31b --- /dev/null +++ b/app/utils.py @@ -0,0 +1,42 @@ +# This can be removed when using Python >= 3.10 (for List at least; the rest idk). +from typing import cast, Dict, List + + + +class Utils: + + @staticmethod + def is_dict_with_string_keys(variable: object) -> bool: + """Casts the given object to a dictionary with string keys; raises the given exception if the given object is not that.""" + return isinstance(variable, dict) and all(isinstance(elem, str) for elem in variable.keys()) + + @staticmethod + def checked_cast_to_string(variable: object, exception: Exception = ValueError("Given object is not a string.")) -> str: + """Casts the given object to a string; raises the given exception if the given object is not that.""" + if not isinstance(object, str): + raise exception + return cast(str, object) + + @staticmethod + def checked_cast_to_list_of_strings(variable: object, exception: Exception = ValueError("Given object is not a list of strings.")) -> List[str]: + """Casts the given object to a list of strings; raises the given exception if the given object is not that.""" + if not isinstance(variable, list) or not all(isinstance(elem, str) for elem in variable): + raise exception + return cast(List[str], object) + + @staticmethod + def checked_cast_to_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a dictionary with string keys.")) -> Dict[str, object]: + """Casts the given object to a dictionary with string keys; raises the given exception if the given object is not that.""" + if not Utils.is_dict_with_string_keys(object): + raise exception + return cast(Dict[str, object], object) + + @staticmethod + def checked_cast_to_list_of_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a list of dictionaries with string keys.")) -> List[Dict[str, object]]: + """Casts the given object to a list of dictionaries with string keys; raises the given exception if the given object is not a that.""" + if not isinstance(variable, list) or not all(Utils.is_dict_with_string_keys(elem) for elem in variable): + raise exception + return cast(List[Dict[str, object]], object) + + def __new__(cls, *args, **kwargs): + raise RuntimeError("Utils is static and thus cannot be instantiated.") From cb294ba7029674fb13eaebcc3751da26c190a1fb Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:14:18 +0200 Subject: [PATCH 080/128] Create ModelineInstructionsMapping class --- Sublime Modelines.sublime-settings | 4 +- app/modeline_instructions_mapping.py | 131 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 app/modeline_instructions_mapping.py diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 5a3d01f..a267e7d 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -76,7 +76,7 @@ "autoindent": {"aliases": ["ai"], "key": "auto_indent", "value": true}, "noautoindent": {"aliases": ["noai"], "key": "auto_indent", "value": false}, /* Set line endings (DOS, Legacy MacOS, UNIX). */ - "fileformat": {"aliases": ["ff"], "key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR"/* unix is not needed, the value is the same */}}, + "fileformat": {"aliases": ["ff"], "key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR", "unix": "unix"}}, /* Set the syntax of the file. */ "filetype": {"aliases": ["ft"], "key": "x_syntax"}, /* # of columns for each tab character. */ @@ -169,7 +169,7 @@ * From . */ "emacs_mapping": { /* Set line endings (DOS, Legacy MacOS, UNIX). */ - "coding": {"key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR"/* unix is not needed, the value is the same */}}, + "coding": {"key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR", "unix": "unix"}}, /* Tabs → Spaces enable/disable. */ "indent-tabs-mode": {"key": "translate_tabs_to_spaces", "value-mapping": {"nil": true, "0": true}, "value-mapping-default": false}, /* Set the syntax of the file. */ diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py new file mode 100644 index 0000000..8cadd33 --- /dev/null +++ b/app/modeline_instructions_mapping.py @@ -0,0 +1,131 @@ +# This can be removed when using Python >= 3.10 (for List at least; the rest idk). +from typing import Dict, List, Optional + +from abc import ABC, abstractmethod + +from .logger import Logger +from .utils import Utils + + + +class ModelineInstructionsMapping: + + class MappingValue: + + class ValueTransform(ABC): + + @abstractmethod + def __init__(self, parameters: Dict[str, object]) -> None: + pass + + @abstractmethod + def apply(self, str: str) -> Optional[object]: + pass + + + class ValueTransformLowercase(ValueTransform): + + def __init__(self, parameters: Dict[str, object]) -> None: + super().__init__(parameters) + + def apply(self, str: str) -> Optional[object]: + return str.lower() + + + class ValueTransformMapping(ValueTransform): + + mapping: Dict[str, object] + # If there is no mapping for the given value, the default value is returned. + default_on_no_mapping: Optional[object] + + def __init__(self, parameters: Dict[str, object]) -> None: + super().__init__(parameters) + + if not "table" in parameters: + raise ValueError("Mandatory parameter “table” not present for a “map” transform.") + self.mapping = Utils.checked_cast_to_dict_with_string_keys( + parameters["table"], + ValueError("Invalid “table” value: not a dictionary with string keys.") + ) + self.default_on_no_mapping = parameters["default"] if "default" in parameters else None + + def apply(self, str: str) -> Optional[object]: + return self.mapping[str] if str in self.mapping else self.default_on_no_mapping + + + # This is `None` if the mapped instruction is unsupported (e.g. vim’s “softtab” which is unsupported in Sublime). + # If this is `None`, all the other parameters should be ignored. + key: Optional[str] + # If this is set, the value for the mapped instruction should be unset, and will be overridden by this value. + value: Optional[object] + # These transforms will be applied to the value. + value_transforms: List[ValueTransform] + + def __init__(self, raw_mapping_value: Dict[str, object]) -> None: + super().__init__() + + key = raw_mapping_value["key"] + if key is None: + self.key = None + self.value = None + self.value_transforms = [] + return + + self.key = Utils.checked_cast_to_string(key, ValueError("Invalid “key” value: not a string.")) + # Note: We do not differentiate a None value and the absence of a value. + self.value = raw_mapping_value["value"] if "value" in raw_mapping_value else None + + # Parse transforms shortcut (`value-mapping`). + raw_value_transforms: List[Dict[str, object]] + if "value-mapping" in raw_mapping_value: + if "value-transforms" in raw_mapping_value: + raise ValueError("“value-transforms” must not be in mapping if “value-mapping” exists.") + + raw_value_transforms = [{ + "type": "map", + "parameters": { + "table": Utils.checked_cast_to_dict_with_string_keys( + raw_mapping_value["value-mapping"], + ValueError("Invalid “value-mapping” value: not a dictionary with string keys.") + ), + "default": raw_mapping_value["value-mapping-default"] if "value-mapping-default" in raw_mapping_value else None + } + }] + + else: + raw_value_transforms = Utils.checked_cast_to_list_of_dict_with_string_keys( + raw_mapping_value["value-transforms"], + ValueError("") + ) if "value-transforms" in raw_mapping_value else [] + + # Parse transforms from `raw_value_transforms`. + self.value_transforms = [] + for raw_value_transform in raw_value_transforms: + params: Dict[str, object] = Utils.checked_cast_to_dict_with_string_keys( + raw_value_transform["parameters"], + ValueError("Invalid “parameters” for a value transform: not a dictionary with string keys.") + ) if "parameters" in raw_value_transform else {} + match Utils.checked_cast_to_string(raw_value_transform["type"]) if "type" in raw_value_transform else None: + case "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) + case "map": self.value_transforms.append(self.ValueTransformMapping(params)) + case _: raise ValueError("Invalid/unknown type for a value transform.") + + + mapping: Dict[str, MappingValue] = {} + + def __init__(self, raw_mapping_object: Dict[str, Dict[str, object]] = {}) -> None: + super().__init__() + + for key, val in raw_mapping_object.items(): + try: + aliases = Utils.checked_cast_to_list_of_strings( + val["aliases"] if "aliases" in val else [], + ValueError("Invalid “aliases” value: not a list of strings.") + ) + + val = ModelineInstructionsMapping.MappingValue(val) + for key in [key] + aliases: + self.mapping[key] = val + + except ValueError as e: + Logger.warning(f"Skipping invalid mapping value for key “{key}”: “{e}”.") From 235bd81a33f424e25a432bc1f94cdc6a59cba6b6 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:15:18 +0200 Subject: [PATCH 081/128] Add first model --- app/modeline.py | 13 +++++++++++++ app/modeline_instruction.py | 9 +++++++++ app/modeline_parser.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 app/modeline.py create mode 100644 app/modeline_instruction.py create mode 100644 app/modeline_parser.py diff --git a/app/modeline.py b/app/modeline.py new file mode 100644 index 0000000..cdb0174 --- /dev/null +++ b/app/modeline.py @@ -0,0 +1,13 @@ +# This can be removed when using Python >= 3.10. +from typing import List + +from .modeline_instruction import ModelineInstruction + + + +class Modeline: + + instructions: List[ModelineInstruction] + + def __init__(self): + super().__init__() diff --git a/app/modeline_instruction.py b/app/modeline_instruction.py new file mode 100644 index 0000000..1f7204c --- /dev/null +++ b/app/modeline_instruction.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + + + +class ModelineInstruction(ABC): + + @abstractmethod + def apply(self) -> None: + pass diff --git a/app/modeline_parser.py b/app/modeline_parser.py new file mode 100644 index 0000000..754243c --- /dev/null +++ b/app/modeline_parser.py @@ -0,0 +1,35 @@ +# This can be removed when using Python >= 3.10 (for List at least; the rest idk). +from typing import final, Dict, Optional + +from abc import ABC, abstractmethod + +from .modeline import Modeline +from .modeline_instructions_mapping import ModelineInstructionsMapping + + + +class ModelineParser(ABC): + + def __init__(self): + super().__init__() + + # Concrete sub-classes should set the value of this variable if they have a custom mapping (e.g. for the vim format, “filetype” -> “x_syntax”). + mapping = ModelineInstructionsMapping() + + @final + def parse(self, line: str) -> Optional[Modeline]: + instructions = self.parseRaw(line) + if instructions is None: + return None + + for key, val in instructions.items(): + + raise RuntimeError("Not Implemented") + + @abstractmethod + def parseRaw(self, line: str) -> Optional[Dict[str, str]]: + """ + Abstract method whose concrete implementation should parse the given line as a dictionary of key/values if the line is a modeline. + No parsing of any sort should be done on the key or value, including mappings; this will be handled by the `parse` function (which calls that function). + """ + pass From aaded51fe414b59e7eb784c00bf656a8b874a080 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 22:16:08 +0200 Subject: [PATCH 082/128] =?UTF-8?q?Use=20`get`=20instead=20of=20`=E2=80=A6?= =?UTF-8?q?=20if=20=E2=80=A6=20else=20None`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modeline_instructions_mapping.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 8cadd33..6a01ffc 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -47,7 +47,7 @@ def __init__(self, parameters: Dict[str, object]) -> None: parameters["table"], ValueError("Invalid “table” value: not a dictionary with string keys.") ) - self.default_on_no_mapping = parameters["default"] if "default" in parameters else None + self.default_on_no_mapping = parameters.get("default") def apply(self, str: str) -> Optional[object]: return self.mapping[str] if str in self.mapping else self.default_on_no_mapping @@ -73,7 +73,7 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: self.key = Utils.checked_cast_to_string(key, ValueError("Invalid “key” value: not a string.")) # Note: We do not differentiate a None value and the absence of a value. - self.value = raw_mapping_value["value"] if "value" in raw_mapping_value else None + self.value = raw_mapping_value.get("value") # Parse transforms shortcut (`value-mapping`). raw_value_transforms: List[Dict[str, object]] @@ -88,7 +88,7 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: raw_mapping_value["value-mapping"], ValueError("Invalid “value-mapping” value: not a dictionary with string keys.") ), - "default": raw_mapping_value["value-mapping-default"] if "value-mapping-default" in raw_mapping_value else None + "default": raw_mapping_value.get("value-mapping-default") } }] From 4c3b9c8abc17efdb4cf8979897d864ecb465ac35 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:32:10 +0200 Subject: [PATCH 083/128] Add new util method in settings to convert a setting to a dictionary --- app/settings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/settings.py b/app/settings.py index c677a18..3204891 100644 --- a/app/settings.py +++ b/app/settings.py @@ -88,3 +88,21 @@ def log_to_tmp(self) -> bool: Logger.warning("Did not get a bool in the settings for the log_to_tmp key.") return False return raw_value + + + def __settings_getdict(self, key: str) -> Optional[Dict[str, object]]: + """ + Get the dictionary value for the setting for the given key, if it is dict like. + The whole thing is probably slow af but I don’t know how to do better, nor do I really care tbh. + """ + setting_value = self.settings.get(key) + + # Check if value is dict-like (has “items” function). + # From . + items_attr = getattr(setting_value, "items", None) + if not callable(items_attr): + return None + + ret: Dict[str, object] = {} + for k, v in cast(Dict[str, object], setting_value).items(): + ret[k] = v From 9ed20739b9dcd3fc19fa7702b452d10068f13c56 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:34:01 +0200 Subject: [PATCH 084/128] Fix compilation on Python 3.8 --- app/modeline_instructions_mapping.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 6a01ffc..4e583e6 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -105,10 +105,11 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: raw_value_transform["parameters"], ValueError("Invalid “parameters” for a value transform: not a dictionary with string keys.") ) if "parameters" in raw_value_transform else {} - match Utils.checked_cast_to_string(raw_value_transform["type"]) if "type" in raw_value_transform else None: - case "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) - case "map": self.value_transforms.append(self.ValueTransformMapping(params)) - case _: raise ValueError("Invalid/unknown type for a value transform.") + # The match instruction has been added to Python 3.10 only. + type = Utils.checked_cast_to_string(raw_value_transform["type"]) if ("type" in raw_value_transform) else None + if type == "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) + elif type == "map": self.value_transforms.append(self.ValueTransformMapping(params)) + else: raise ValueError("Invalid/unknown type for a value transform.") mapping: Dict[str, MappingValue] = {} From 08ccb3f8a4da81c8ef5309a4a9b4692eb03f8ac3 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:34:32 +0200 Subject: [PATCH 085/128] Fix signature of init of ModelineInstructionsMapping --- app/modeline_instructions_mapping.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 4e583e6..4deac07 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -114,10 +114,13 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: mapping: Dict[str, MappingValue] = {} - def __init__(self, raw_mapping_object: Dict[str, Dict[str, object]] = {}) -> None: + def __init__(self, raw_mapping_object: Dict[str, Dict[str, Optional[object]]] = {}) -> None: super().__init__() for key, val in raw_mapping_object.items(): + # We must silently skip None values as these are valid overrides for user mappings, to remove a specific mapping. + if val is None: continue + try: aliases = Utils.checked_cast_to_list_of_strings( val["aliases"] if "aliases" in val else [], From ace2fe43a0146216f80909e7bcc7a7661d9bd26a Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:35:33 +0200 Subject: [PATCH 086/128] Utils: Add a new method to merge two dictionaries --- app/utils.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/utils.py b/app/utils.py index 1bfb31b..ee3c2a0 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,5 @@ # This can be removed when using Python >= 3.10 (for List at least; the rest idk). -from typing import cast, Dict, List +from typing import cast, Dict, List, TypeVar @@ -38,5 +38,23 @@ def checked_cast_to_list_of_dict_with_string_keys(variable: object, exception: E raise exception return cast(List[Dict[str, object]], object) + K = TypeVar("K"); V = TypeVar("V") + @staticmethod + def merge(a: Dict[K, V], b: Dict[K, V], path=[]) -> Dict[K, V]: + """ + Merges b in a, in place, and returns a. + From , modified (and not extensively tested…). + """ + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + Utils.merge(cast(Dict[object, object], a[key]), cast(Dict[object, object], b[key]), path + [str(key)]) + else: + # Original SO source checked whether the values were the same; we do not care and just trump. + a[key] = b[key] + else: + a[key] = b[key] + return a + def __new__(cls, *args, **kwargs): raise RuntimeError("Utils is static and thus cannot be instantiated.") From c4639e7e10c40f01d5a51870e82e761f44cc0adf Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:36:25 +0200 Subject: [PATCH 087/128] Utils: Add a new method to cast an object to a dict of dict (both w/ string keys) --- app/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/utils.py b/app/utils.py index ee3c2a0..8521f73 100644 --- a/app/utils.py +++ b/app/utils.py @@ -38,6 +38,14 @@ def checked_cast_to_list_of_dict_with_string_keys(variable: object, exception: E raise exception return cast(List[Dict[str, object]], object) + @staticmethod + def checked_cast_to_dict_of_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a dictionary with string keys of dictionaries with string keys.")) -> Dict[str, Dict[str, object]]: + """Casts the given object to a dictionary with string key of dictionaries with string keys; raises the given exception if the given object is not a that.""" + dict = Utils.checked_cast_to_dict_with_string_keys(variable, exception) + if not all(Utils.is_dict_with_string_keys(elem) for elem in dict.values()): + raise exception + return cast(Dict[str, Dict[str, object]], variable) + K = TypeVar("K"); V = TypeVar("V") @staticmethod def merge(a: Dict[K, V], b: Dict[K, V], path=[]) -> Dict[K, V]: From e7acd020c967355221ea274ae871dfb719a90d8d Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:36:53 +0200 Subject: [PATCH 088/128] Utils: Fix incorrect variable used throughout the file --- app/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/utils.py b/app/utils.py index 8521f73..f280497 100644 --- a/app/utils.py +++ b/app/utils.py @@ -13,30 +13,30 @@ def is_dict_with_string_keys(variable: object) -> bool: @staticmethod def checked_cast_to_string(variable: object, exception: Exception = ValueError("Given object is not a string.")) -> str: """Casts the given object to a string; raises the given exception if the given object is not that.""" - if not isinstance(object, str): + if not isinstance(variable, str): raise exception - return cast(str, object) + return cast(str, variable) @staticmethod def checked_cast_to_list_of_strings(variable: object, exception: Exception = ValueError("Given object is not a list of strings.")) -> List[str]: """Casts the given object to a list of strings; raises the given exception if the given object is not that.""" if not isinstance(variable, list) or not all(isinstance(elem, str) for elem in variable): raise exception - return cast(List[str], object) + return cast(List[str], variable) @staticmethod def checked_cast_to_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a dictionary with string keys.")) -> Dict[str, object]: """Casts the given object to a dictionary with string keys; raises the given exception if the given object is not that.""" - if not Utils.is_dict_with_string_keys(object): + if not Utils.is_dict_with_string_keys(variable): raise exception - return cast(Dict[str, object], object) + return cast(Dict[str, object], variable) @staticmethod def checked_cast_to_list_of_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a list of dictionaries with string keys.")) -> List[Dict[str, object]]: """Casts the given object to a list of dictionaries with string keys; raises the given exception if the given object is not a that.""" if not isinstance(variable, list) or not all(Utils.is_dict_with_string_keys(elem) for elem in variable): raise exception - return cast(List[Dict[str, object]], object) + return cast(List[Dict[str, object]], variable) @staticmethod def checked_cast_to_dict_of_dict_with_string_keys(variable: object, exception: Exception = ValueError("Given object is not a dictionary with string keys of dictionaries with string keys.")) -> Dict[str, Dict[str, object]]: From fc8a932cfe0f643bc499516c9ff4b1dcdb287a53 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:37:54 +0200 Subject: [PATCH 089/128] Settings: Add VIM mapping retrieval --- Sublime Modelines.sublime-settings | 4 ++-- app/settings.py | 35 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index a267e7d..ee0f85a 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -161,7 +161,7 @@ }}}, ]}, }, - /* User mapping for VIM modelines. */ + /* User mapping for VIM modelines (deep-merged for dictionaries; everything else is replaced; set a key to null to remove it from the default mapping). */ "vim_mapping_user": {}, /* Default Emacs commands mapping. @@ -177,7 +177,7 @@ /* # of columns for each tab character. */ "tab-width": {"key": "tab_size"}, }, - /* User mapping for Emacs modelines. */ + /* User mapping for Emacs modelines (deep-merged for dictionaries; everything else is replaced; set a key to null to remove it from the default mapping). */ "emacs_mapping_user": {}, /* Whether debug logs should be enabled. */ diff --git a/app/settings.py b/app/settings.py index 3204891..e435675 100644 --- a/app/settings.py +++ b/app/settings.py @@ -5,6 +5,8 @@ import sublime from .logger import Logger +from .modeline_instructions_mapping import ModelineInstructionsMapping +from .utils import Utils @@ -75,6 +77,21 @@ def number_of_lines_to_check_from_end(self) -> int: return 5 return raw_value + def vimMapping(self) -> ModelineInstructionsMapping: + raw_value = Utils.checked_cast_to_dict_with_string_keys( + self.settings.get("vim_mapping"), + ValueError("Invalid “vim_mapping” setting value: not a dict with string keys.") + ) + raw_value_user = Utils.checked_cast_to_dict_with_string_keys( + self.settings.get("vim_mapping_user"), + ValueError("Invalid “vim_mapping_user” setting value: not a dict with string keys.") + ) + raw_value = Utils.checked_cast_to_dict_of_dict_with_string_keys( + Utils.merge(raw_value, raw_value_user), + ValueError("Invalid “vim_mapping” or “vim_mapping_user”: the resulting merged dictionary is not a dictionary with string keys of dictionary with string keys.") + ) + return ModelineInstructionsMapping(raw_value) + def verbose(self) -> bool: raw_value = self.settings.get("verbose") if not isinstance(raw_value, bool): @@ -88,21 +105,3 @@ def log_to_tmp(self) -> bool: Logger.warning("Did not get a bool in the settings for the log_to_tmp key.") return False return raw_value - - - def __settings_getdict(self, key: str) -> Optional[Dict[str, object]]: - """ - Get the dictionary value for the setting for the given key, if it is dict like. - The whole thing is probably slow af but I don’t know how to do better, nor do I really care tbh. - """ - setting_value = self.settings.get(key) - - # Check if value is dict-like (has “items” function). - # From . - items_attr = getattr(setting_value, "items", None) - if not callable(items_attr): - return None - - ret: Dict[str, object] = {} - for k, v in cast(Dict[str, object], setting_value).items(): - ret[k] = v From 850ba51fdfc549cf899453f2ce1dc364fad92775 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:50:19 +0200 Subject: [PATCH 090/128] Implement pretty-printing ModelineInstructionsMapping --- app/modeline_instructions_mapping.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 4deac07..c41476f 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -110,6 +110,9 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: if type == "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) elif type == "map": self.value_transforms.append(self.ValueTransformMapping(params)) else: raise ValueError("Invalid/unknown type for a value transform.") + + def __str__(self) -> str: + return f"\tkey: {self.key}\n\tvalue: {self.value}\n\ttransforms_count: {len(self.value_transforms)}" mapping: Dict[str, MappingValue] = {} @@ -133,3 +136,11 @@ def __init__(self, raw_mapping_object: Dict[str, Dict[str, Optional[object]]] = except ValueError as e: Logger.warning(f"Skipping invalid mapping value for key “{key}”: “{e}”.") + + def __str__(self) -> str: + # There is probably a more Pythonic way of doing this (map + join?), but this works. + res = "" + for k, v in self.mapping.items(): + res += k + ":\n" + v.__str__() + res += "\n" + return res From f59e71c283b85f88ce5854093dca5bdc8af3f6d5 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Mon, 2 Mar 2026 23:51:45 +0200 Subject: [PATCH 091/128] Settings: Add Emacs mapping retrieval --- app/settings.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/settings.py b/app/settings.py index e435675..b380c83 100644 --- a/app/settings.py +++ b/app/settings.py @@ -92,6 +92,21 @@ def vimMapping(self) -> ModelineInstructionsMapping: ) return ModelineInstructionsMapping(raw_value) + def emacsMapping(self) -> ModelineInstructionsMapping: + raw_value = Utils.checked_cast_to_dict_with_string_keys( + self.settings.get("emacs_mapping"), + ValueError("Invalid “emacs_mapping” setting value: not a dict with string keys.") + ) + raw_value_user = Utils.checked_cast_to_dict_with_string_keys( + self.settings.get("emacs_mapping_user"), + ValueError("Invalid “emacs_mapping_user” setting value: not a dict with string keys.") + ) + raw_value = Utils.checked_cast_to_dict_of_dict_with_string_keys( + Utils.merge(raw_value, raw_value_user), + ValueError("Invalid “emacs_mapping” or “emacs_mapping_user”: the resulting merged dictionary is not a dictionary with string keys of dictionary with string keys.") + ) + return ModelineInstructionsMapping(raw_value) + def verbose(self) -> bool: raw_value = self.settings.get("verbose") if not isinstance(raw_value, bool): From 82d2b903a0df6c3ef881d9ef103eba58ec83245f Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:03:06 +0200 Subject: [PATCH 092/128] Update settings file with enhanced parsing rules --- Sublime Modelines.sublime-settings | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index ee0f85a..d8d9c99 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -13,7 +13,7 @@ "formats": [ /* Default format. * Examples: - * `// ~*~ sublime: key=val; key2=val2 ~*~` + * `// ~*~ sublime: key=val; key2=val2; key3 ~*~` * `// ~*~ sublime: key = val; key2+=val2; ~*~` * `// ~*~ sublime: key=["hello": "world"] ~*~` * (Also works with /*-styled comments, but JSON does not supported nested comments, so I’m skipping this example…) @@ -21,8 +21,10 @@ * This format is inspired by VIM (`sublime:` prefix, key=val) as well as Emacs (`~*~` delimiters; Emacs uses `-*-`). * * Any value that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) is parsed as a JSON string. + * If the JSON fails to parse, the original string is kept. * The literal strings `true` and `false` are converted to their boolean values. - * Literal numbers (`42`, `3.14`, `-007`, `+12.345`) are converted to numbers. + * Literal numbers (`42`, `3.14`, `-007`, `+12.345`, `23e32`) are converted to ints or floats. + * The exact rule is: if `int(value)` does not throw, the int value is used, then if `float(value)` does not throw, the float value is used. * The literal string `null` is converted to None. * You can use double-quotes for these cases if you need an explicit string instead. * From f4f036f4e96ac3072ff5e9e68660ac48a422eb91 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:11:05 +0200 Subject: [PATCH 093/128] Use get method on dict in more places --- app/modeline_instructions_mapping.py | 14 +++++++------- app/utils.py | 9 ++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index c41476f..47a050d 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -50,7 +50,7 @@ def __init__(self, parameters: Dict[str, object]) -> None: self.default_on_no_mapping = parameters.get("default") def apply(self, str: str) -> Optional[object]: - return self.mapping[str] if str in self.mapping else self.default_on_no_mapping + return self.mapping.get(value, self.default_on_no_mapping) # This is `None` if the mapped instruction is unsupported (e.g. vim’s “softtab” which is unsupported in Sublime). @@ -94,19 +94,19 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: else: raw_value_transforms = Utils.checked_cast_to_list_of_dict_with_string_keys( - raw_mapping_value["value-transforms"], + raw_mapping_value.get("value-transforms", []), ValueError("") - ) if "value-transforms" in raw_mapping_value else [] + ) # Parse transforms from `raw_value_transforms`. self.value_transforms = [] for raw_value_transform in raw_value_transforms: params: Dict[str, object] = Utils.checked_cast_to_dict_with_string_keys( - raw_value_transform["parameters"], + raw_value_transform.get("parameters", {}), ValueError("Invalid “parameters” for a value transform: not a dictionary with string keys.") - ) if "parameters" in raw_value_transform else {} + ) # The match instruction has been added to Python 3.10 only. - type = Utils.checked_cast_to_string(raw_value_transform["type"]) if ("type" in raw_value_transform) else None + type = Utils.checked_cast_to_optional_string(raw_value_transform.get("type")) if type == "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) elif type == "map": self.value_transforms.append(self.ValueTransformMapping(params)) else: raise ValueError("Invalid/unknown type for a value transform.") @@ -126,7 +126,7 @@ def __init__(self, raw_mapping_object: Dict[str, Dict[str, Optional[object]]] = try: aliases = Utils.checked_cast_to_list_of_strings( - val["aliases"] if "aliases" in val else [], + val.get("aliases", []), ValueError("Invalid “aliases” value: not a list of strings.") ) diff --git a/app/utils.py b/app/utils.py index f280497..a48bc90 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,5 @@ # This can be removed when using Python >= 3.10 (for List at least; the rest idk). -from typing import cast, Dict, List, TypeVar +from typing import cast, Dict, List, Optional, TypeVar @@ -17,6 +17,13 @@ def checked_cast_to_string(variable: object, exception: Exception = ValueError(" raise exception return cast(str, variable) + @staticmethod + def checked_cast_to_optional_string(variable: object, exception: Exception = ValueError("Given object is not an optional string.")) -> Optional[str]: + """Casts the given object to an optional string; raises the given exception if the given object is not that.""" + if object is None: + return None + return Utils.checked_cast_to_string(variable, exception) + @staticmethod def checked_cast_to_list_of_strings(variable: object, exception: Exception = ValueError("Given object is not a list of strings.")) -> List[str]: """Casts the given object to a list of strings; raises the given exception if the given object is not that.""" From 44e2d547d40f5ed5d136f9209538736626927591 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:11:46 +0200 Subject: [PATCH 094/128] Apply transforms on optional objects instead of strings --- app/modeline_instructions_mapping.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 47a050d..b979211 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -19,7 +19,7 @@ def __init__(self, parameters: Dict[str, object]) -> None: pass @abstractmethod - def apply(self, str: str) -> Optional[object]: + def apply(self, value: Optional[object]) -> Optional[object]: pass @@ -28,8 +28,11 @@ class ValueTransformLowercase(ValueTransform): def __init__(self, parameters: Dict[str, object]) -> None: super().__init__(parameters) - def apply(self, str: str) -> Optional[object]: - return str.lower() + def apply(self, value: Optional[object]) -> Optional[object]: + if not isinstance(value, str): + Logger.warning(f"Skipping lowercase transform for value “{value}” because it is not a string.") + return None + return value.lower() class ValueTransformMapping(ValueTransform): @@ -49,7 +52,10 @@ def __init__(self, parameters: Dict[str, object]) -> None: ) self.default_on_no_mapping = parameters.get("default") - def apply(self, str: str) -> Optional[object]: + def apply(self, value: Optional[object]) -> Optional[object]: + if not isinstance(value, str): + Logger.warning(f"Skipping lowercase transform for value “{value}” because it is not a string.") + return None return self.mapping.get(value, self.default_on_no_mapping) From 8cc715dde80c1d6bbb4661693a8a98cf3064e072 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:12:02 +0200 Subject: [PATCH 095/128] Add new utilities methods --- app/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/utils.py b/app/utils.py index a48bc90..e0b0fd1 100644 --- a/app/utils.py +++ b/app/utils.py @@ -53,6 +53,16 @@ def checked_cast_to_dict_of_dict_with_string_keys(variable: object, exception: E raise exception return cast(Dict[str, Dict[str, object]], variable) + @staticmethod + def as_int_or_none(variable: str) -> Optional[int]: + try: return int(variable) + except ValueError: return None + + @staticmethod + def as_float_or_none(variable: str) -> Optional[float]: + try: return float(variable) + except ValueError: return None + K = TypeVar("K"); V = TypeVar("V") @staticmethod def merge(a: Dict[K, V], b: Dict[K, V], path=[]) -> Dict[K, V]: From a1355b90418bd16c2144393c1b321228372d3ea9 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:16:33 +0200 Subject: [PATCH 096/128] Add method to apply the mapping on a key/value --- app/modeline_instructions_mapping.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index b979211..3de9996 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -1,5 +1,5 @@ # This can be removed when using Python >= 3.10 (for List at least; the rest idk). -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from abc import ABC, abstractmethod @@ -150,3 +150,22 @@ def __str__(self) -> str: res += k + ":\n" + v.__str__() res += "\n" return res + + # Returns `None` if the mapping tells the key is unsupported. + def apply(self, key: str, value: Optional[object]) -> Optional[Tuple[str, Optional[object]]]: + mapping_value = self.mapping.get(key) + if mapping_value is None: return (key, value) + + # If there is a None key in the mapping value, the key is unsupported: we return None. + if mapping_value.key is None: + return None + key = mapping_value.key + + # Replace the value if the mapping has a forced value. + if not mapping_value.value is None: + if not value is None: + Logger.warning(f"Replacing value “{value}” for key “{key}” with “{mapping_value.value}”: the key is mapped with a forced value.") + value = mapping_value.value + + for transform in mapping_value.value_transforms: + value = transform.apply(value) From fde1bcfb92ce605b163d0e317dca99bf357b152d Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 00:17:26 +0200 Subject: [PATCH 097/128] Start implementation of raw line parsing result to a Modeline --- app/modeline_parser.py | 51 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 754243c..7855aef 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -1,15 +1,23 @@ # This can be removed when using Python >= 3.10 (for List at least; the rest idk). -from typing import final, Dict, Optional +from typing import final, List, Optional, Tuple from abc import ABC, abstractmethod +from enum import Enum +import json from .modeline import Modeline from .modeline_instructions_mapping import ModelineInstructionsMapping +from .utils import Utils class ModelineParser(ABC): + class ValueModifier(str, Enum): + NONE = "" + ADD = "+" + REMOVE = "-" + def __init__(self): super().__init__() @@ -17,19 +25,50 @@ def __init__(self): mapping = ModelineInstructionsMapping() @final - def parse(self, line: str) -> Optional[Modeline]: - instructions = self.parseRaw(line) + def parse_line(self, line: str) -> Optional[Modeline]: + instructions = self.parse_line_raw(line) if instructions is None: return None - for key, val in instructions.items(): + for key, value, modifier in instructions: + # Let’s parse the value. + # See the Sublime settings file for the rules (and update it if they change). + if not value is None: + if j:= self.__parse_jsonesque_str(value): value = j + elif value == "true": value = True + elif value == "false": value = False + elif i := Utils.as_int_or_none (value): value = i + elif f := Utils.as_float_or_none(value): value = f + elif value == "null": value = None + + # Apply the mapping to the key and value. + key_value = self.mapping.apply(key, value) + if key_value is None: return None # Unsupported key + (key, value) = key_value - raise RuntimeError("Not Implemented") + # Apply the post-mapping transform on the key. + key = self.transform_key_post_mapping(key) @abstractmethod - def parseRaw(self, line: str) -> Optional[Dict[str, str]]: + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ValueModifier]]]: """ Abstract method whose concrete implementation should parse the given line as a dictionary of key/values if the line is a modeline. No parsing of any sort should be done on the key or value, including mappings; this will be handled by the `parse` function (which calls that function). """ pass + + def transform_key_post_mapping(self, key: str) -> str: + """ + Gives an opportunity to concrete sub-classes to post-process the key after the mapping has been applied. + This is used for instance by the VIM modeline parser class to implement Sublime commands with a prefix. + """ + return key + + + # Parse strings that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) as a JSON string. + def __parse_jsonesque_str(self, str: str) -> Optional[object]: + if not str.startswith('"') and not str.startswith('{') and not str.startswith('['): + return None + + try: return json.loads(str) + except json.decoder.JSONDecodeError: return None From db58602df38929638aeba88391b6bf14b05ceeda Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 15:46:23 +0200 Subject: [PATCH 098/128] Start Modeline instruction parsing --- .../modeline_instruction__call_view_function.py | 12 ++++++++++++ .../modeline_instruction__set_view_setting.py | 12 ++++++++++++ app/modeline_parser.py | 11 ++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 app/modeline_instructions/modeline_instruction__call_view_function.py create mode 100644 app/modeline_instructions/modeline_instruction__set_view_setting.py diff --git a/app/modeline_instructions/modeline_instruction__call_view_function.py b/app/modeline_instructions/modeline_instruction__call_view_function.py new file mode 100644 index 0000000..14b1308 --- /dev/null +++ b/app/modeline_instructions/modeline_instruction__call_view_function.py @@ -0,0 +1,12 @@ +from typing import final + +from ..modeline_instruction import ModelineInstruction + + + +@final +class ModelineInstruction_CallViewFunction(ModelineInstruction): + + def apply(self) -> None: + # TODO + raise Exception("Not implemented") diff --git a/app/modeline_instructions/modeline_instruction__set_view_setting.py b/app/modeline_instructions/modeline_instruction__set_view_setting.py new file mode 100644 index 0000000..b7b9789 --- /dev/null +++ b/app/modeline_instructions/modeline_instruction__set_view_setting.py @@ -0,0 +1,12 @@ +from typing import final + +from ..modeline_instruction import ModelineInstruction + + + +@final +class ModelineInstruction_SetViewSetting(ModelineInstruction): + + def apply(self) -> None: + # TODO + raise Exception("Not implemented") diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 7855aef..6c2c26c 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -26,12 +26,14 @@ def __init__(self): @final def parse_line(self, line: str) -> Optional[Modeline]: - instructions = self.parse_line_raw(line) - if instructions is None: + instructions_raw = self.parse_line_raw(line) + if instructions_raw is None: return None - for key, value, modifier in instructions: + res = Modeline() + for key, value, modifier in instructions_raw: # Let’s parse the value. + # It should already be trimmed (`parse_line_raw` should do it). # See the Sublime settings file for the rules (and update it if they change). if not value is None: if j:= self.__parse_jsonesque_str(value): value = j @@ -48,12 +50,15 @@ def parse_line(self, line: str) -> Optional[Modeline]: # Apply the post-mapping transform on the key. key = self.transform_key_post_mapping(key) + + # TODO @abstractmethod def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ValueModifier]]]: """ Abstract method whose concrete implementation should parse the given line as a dictionary of key/values if the line is a modeline. No parsing of any sort should be done on the key or value, including mappings; this will be handled by the `parse` function (which calls that function). + If applicable, trimming should be done by this function though. """ pass From 97058075fbafba1c7c52149d5249cf1c92074d64 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:25:59 +0200 Subject: [PATCH 099/128] Replace `Optional[object]` by `object` `object` is already optional in itself. --- app/modeline_instructions_mapping.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 3de9996..d561d95 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -19,7 +19,7 @@ def __init__(self, parameters: Dict[str, object]) -> None: pass @abstractmethod - def apply(self, value: Optional[object]) -> Optional[object]: + def apply(self, value: object) -> object: pass @@ -28,7 +28,7 @@ class ValueTransformLowercase(ValueTransform): def __init__(self, parameters: Dict[str, object]) -> None: super().__init__(parameters) - def apply(self, value: Optional[object]) -> Optional[object]: + def apply(self, value: object) -> object: if not isinstance(value, str): Logger.warning(f"Skipping lowercase transform for value “{value}” because it is not a string.") return None @@ -39,7 +39,7 @@ class ValueTransformMapping(ValueTransform): mapping: Dict[str, object] # If there is no mapping for the given value, the default value is returned. - default_on_no_mapping: Optional[object] + default_on_no_mapping: object def __init__(self, parameters: Dict[str, object]) -> None: super().__init__(parameters) @@ -52,7 +52,7 @@ def __init__(self, parameters: Dict[str, object]) -> None: ) self.default_on_no_mapping = parameters.get("default") - def apply(self, value: Optional[object]) -> Optional[object]: + def apply(self, value: object) -> object: if not isinstance(value, str): Logger.warning(f"Skipping lowercase transform for value “{value}” because it is not a string.") return None @@ -63,7 +63,7 @@ def apply(self, value: Optional[object]) -> Optional[object]: # If this is `None`, all the other parameters should be ignored. key: Optional[str] # If this is set, the value for the mapped instruction should be unset, and will be overridden by this value. - value: Optional[object] + value: object # These transforms will be applied to the value. value_transforms: List[ValueTransform] @@ -152,7 +152,7 @@ def __str__(self) -> str: return res # Returns `None` if the mapping tells the key is unsupported. - def apply(self, key: str, value: Optional[object]) -> Optional[Tuple[str, Optional[object]]]: + def apply(self, key: str, value: object) -> Optional[Tuple[str, object]]: mapping_value = self.mapping.get(key) if mapping_value is None: return (key, value) From e14ec576b90f8b6b119f509dc74ded17a30825bf Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:26:15 +0200 Subject: [PATCH 100/128] Dummy code organization change --- app/modeline_instructions_mapping.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index d561d95..d8c5c0d 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -154,11 +154,11 @@ def __str__(self) -> str: # Returns `None` if the mapping tells the key is unsupported. def apply(self, key: str, value: object) -> Optional[Tuple[str, object]]: mapping_value = self.mapping.get(key) + # If the mapping value is None, we return the unmodified source. + # If there is a None key in the mapping value, the key is unsupported: we return None. if mapping_value is None: return (key, value) + if mapping_value.key is None: return None - # If there is a None key in the mapping value, the key is unsupported: we return None. - if mapping_value.key is None: - return None key = mapping_value.key # Replace the value if the mapping has a forced value. From 60c7890787e52422b3df07d1189c2fe6756d6559 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:26:42 +0200 Subject: [PATCH 101/128] Properly return the mapped modeline instruction in apply function --- app/modeline_instructions_mapping.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index d8c5c0d..8f47555 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -169,3 +169,5 @@ def apply(self, key: str, value: object) -> Optional[Tuple[str, object]]: for transform in mapping_value.value_transforms: value = transform.apply(value) + + return (key, value) From 33d94d2507edfabcb16020689c25c7b0e9ba5742 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:28:02 +0200 Subject: [PATCH 102/128] Add new utility to safely cast an object to a SublimeValue --- app/utils.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/utils.py b/app/utils.py index e0b0fd1..a856425 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,7 @@ # This can be removed when using Python >= 3.10 (for List at least; the rest idk). -from typing import cast, Dict, List, Optional, TypeVar +from typing import cast, Any, Dict, List, Optional, TypeVar + +from sublime_types import Value as SublimeValue @@ -53,6 +55,16 @@ def checked_cast_to_dict_of_dict_with_string_keys(variable: object, exception: E raise exception return cast(Dict[str, Dict[str, object]], variable) + @staticmethod + def checked_cast_to_sublime_value(variable: object, exception: Exception = ValueError("Given object is not a Sublime Value.")) -> SublimeValue: + """Casts the given object to a Sublime Value; raises the given exception if the given object is not that.""" + # I don’t think there is a way to automatically check all the elements of the Value union, so we do them manually. + # We’ll have to manually update the checks when the Value type is updated in Sublime. + for t in [bool, str, int, float, List[Any], Dict[str, Any], None]: + if isinstance(variable, t): + return cast(SublimeValue, variable) + raise exception + @staticmethod def as_int_or_none(variable: str) -> Optional[int]: try: return int(variable) From 233742f03f518501b02e52eceaedcfb4dcd46c27 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:28:37 +0200 Subject: [PATCH 103/128] Implement the two modeline instructions we have --- app/modeline_instruction.py | 4 +++- .../call_view_function.py | 23 +++++++++++++++++++ ...odeline_instruction__call_view_function.py | 12 ---------- .../modeline_instruction__set_view_setting.py | 12 ---------- app/modeline_instructions/set_view_setting.py | 22 ++++++++++++++++++ 5 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 app/modeline_instructions/call_view_function.py delete mode 100644 app/modeline_instructions/modeline_instruction__call_view_function.py delete mode 100644 app/modeline_instructions/modeline_instruction__set_view_setting.py create mode 100644 app/modeline_instructions/set_view_setting.py diff --git a/app/modeline_instruction.py b/app/modeline_instruction.py index 1f7204c..3a77f5d 100644 --- a/app/modeline_instruction.py +++ b/app/modeline_instruction.py @@ -1,9 +1,11 @@ from abc import ABC, abstractmethod +import sublime + class ModelineInstruction(ABC): @abstractmethod - def apply(self) -> None: + def apply(self, view: sublime.View) -> None: pass diff --git a/app/modeline_instructions/call_view_function.py b/app/modeline_instructions/call_view_function.py new file mode 100644 index 0000000..37cb73b --- /dev/null +++ b/app/modeline_instructions/call_view_function.py @@ -0,0 +1,23 @@ +from typing import final + +from sublime import View as SublimeView +from sublime_types import Value as SublimeValue + +from ..modeline_instruction import ModelineInstruction + + + +@final +class ModelineInstruction_CallViewFunction(ModelineInstruction): + + function_name: str + function_arg: SublimeValue + + def __init__(self, function_name: str, function_arg: SublimeValue) -> None: + super().__init__() + self.function_name = function_name + self.function_arg = function_arg + + def apply(self, view: SublimeView) -> None: + f = getattr(view, self.function_name) + f(self.function_arg) diff --git a/app/modeline_instructions/modeline_instruction__call_view_function.py b/app/modeline_instructions/modeline_instruction__call_view_function.py deleted file mode 100644 index 14b1308..0000000 --- a/app/modeline_instructions/modeline_instruction__call_view_function.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import final - -from ..modeline_instruction import ModelineInstruction - - - -@final -class ModelineInstruction_CallViewFunction(ModelineInstruction): - - def apply(self) -> None: - # TODO - raise Exception("Not implemented") diff --git a/app/modeline_instructions/modeline_instruction__set_view_setting.py b/app/modeline_instructions/modeline_instruction__set_view_setting.py deleted file mode 100644 index b7b9789..0000000 --- a/app/modeline_instructions/modeline_instruction__set_view_setting.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import final - -from ..modeline_instruction import ModelineInstruction - - - -@final -class ModelineInstruction_SetViewSetting(ModelineInstruction): - - def apply(self) -> None: - # TODO - raise Exception("Not implemented") diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py new file mode 100644 index 0000000..a81c0c9 --- /dev/null +++ b/app/modeline_instructions/set_view_setting.py @@ -0,0 +1,22 @@ +from typing import final + +from sublime import View as SublimeView +from sublime_types import Value as SublimeValue + +from ..modeline_instruction import ModelineInstruction + + + +@final +class ModelineInstruction_SetViewSetting(ModelineInstruction): + + setting_name: str + setting_value: SublimeValue + + def __init__(self, setting_name: str, setting_value: SublimeValue) -> None: + super().__init__() + self.setting_name = setting_name + self.setting_value = setting_value + + def apply(self, view: SublimeView) -> None: + view.settings().set(self.setting_name, self.setting_value) From 014a03a65266cf59c37adb9f1498603e11370a08 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:30:19 +0200 Subject: [PATCH 104/128] Implement converting the raw modeline parsing result to an actual modeline --- app/modeline_parser.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 6c2c26c..517ae7f 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -6,6 +6,8 @@ import json from .modeline import Modeline +from .modeline_instructions.set_view_setting import ModelineInstruction_SetViewSetting +from .modeline_instructions.call_view_function import ModelineInstruction_CallViewFunction from .modeline_instructions_mapping import ModelineInstructionsMapping from .utils import Utils @@ -31,27 +33,36 @@ def parse_line(self, line: str) -> Optional[Modeline]: return None res = Modeline() - for key, value, modifier in instructions_raw: + for key, raw_value, modifier in instructions_raw: # Let’s parse the value. # It should already be trimmed (`parse_line_raw` should do it). # See the Sublime settings file for the rules (and update it if they change). - if not value is None: - if j:= self.__parse_jsonesque_str(value): value = j - elif value == "true": value = True - elif value == "false": value = False - elif i := Utils.as_int_or_none (value): value = i - elif f := Utils.as_float_or_none(value): value = f - elif value == "null": value = None + if not raw_value is None: + if j := self.__parse_jsonesque_str(raw_value): value = j + elif raw_value == "true": value = True + elif raw_value == "false": value = False + elif i := Utils.as_int_or_none (raw_value): value = i + elif f := Utils.as_float_or_none(raw_value): value = f + elif raw_value == "null": value = None + else: value = raw_value + else: + value = None # aka. raw_value # Apply the mapping to the key and value. - key_value = self.mapping.apply(key, value) - if key_value is None: return None # Unsupported key - (key, value) = key_value + key_value_pair = self.mapping.apply(key, value) + if key_value_pair is None: return None # Unsupported key + (key, value) = key_value_pair # Apply the post-mapping transform on the key. key = self.transform_key_post_mapping(key) + sublime_value = Utils.checked_cast_to_sublime_value( + value, + ValueError("Post-mapped value is invalid (not a SublimeValue).") + ) - # TODO + if key.endswith("()"): res.instructions.append(ModelineInstruction_CallViewFunction(key[:-2], sublime_value)) + else: res.instructions.append(ModelineInstruction_SetViewSetting (key, sublime_value)) + return res @abstractmethod def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ValueModifier]]]: @@ -71,7 +82,7 @@ def transform_key_post_mapping(self, key: str) -> str: # Parse strings that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) as a JSON string. - def __parse_jsonesque_str(self, str: str) -> Optional[object]: + def __parse_jsonesque_str(self, str: str) -> object: if not str.startswith('"') and not str.startswith('{') and not str.startswith('['): return None From 6cc05a6f754288dff0a6076041aaed9b698b74be Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:49:23 +0200 Subject: [PATCH 105/128] Catch exception when converting raw modeline instructions to structured ones --- app/modeline_parser.py | 60 ++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 517ae7f..56703b1 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -5,6 +5,7 @@ from enum import Enum import json +from .logger import Logger from .modeline import Modeline from .modeline_instructions.set_view_setting import ModelineInstruction_SetViewSetting from .modeline_instructions.call_view_function import ModelineInstruction_CallViewFunction @@ -34,34 +35,37 @@ def parse_line(self, line: str) -> Optional[Modeline]: res = Modeline() for key, raw_value, modifier in instructions_raw: - # Let’s parse the value. - # It should already be trimmed (`parse_line_raw` should do it). - # See the Sublime settings file for the rules (and update it if they change). - if not raw_value is None: - if j := self.__parse_jsonesque_str(raw_value): value = j - elif raw_value == "true": value = True - elif raw_value == "false": value = False - elif i := Utils.as_int_or_none (raw_value): value = i - elif f := Utils.as_float_or_none(raw_value): value = f - elif raw_value == "null": value = None - else: value = raw_value - else: - value = None # aka. raw_value - - # Apply the mapping to the key and value. - key_value_pair = self.mapping.apply(key, value) - if key_value_pair is None: return None # Unsupported key - (key, value) = key_value_pair - - # Apply the post-mapping transform on the key. - key = self.transform_key_post_mapping(key) - sublime_value = Utils.checked_cast_to_sublime_value( - value, - ValueError("Post-mapped value is invalid (not a SublimeValue).") - ) - - if key.endswith("()"): res.instructions.append(ModelineInstruction_CallViewFunction(key[:-2], sublime_value)) - else: res.instructions.append(ModelineInstruction_SetViewSetting (key, sublime_value)) + try: + # Let’s parse the value. + # It should already be trimmed (`parse_line_raw` should do it). + # See the Sublime settings file for the rules (and update it if they change). + if not raw_value is None: + if j := self.__parse_jsonesque_str(raw_value): value = j + elif raw_value == "true": value = True + elif raw_value == "false": value = False + elif i := Utils.as_int_or_none (raw_value): value = i + elif f := Utils.as_float_or_none(raw_value): value = f + elif raw_value == "null": value = None + else: value = raw_value + else: + value = None # aka. raw_value + + # Apply the mapping to the key and value. + key_value_pair = self.mapping.apply(key, value) + if key_value_pair is None: return None # Unsupported key + (key, value) = key_value_pair + + # Apply the post-mapping transform on the key. + key = self.transform_key_post_mapping(key) + sublime_value = Utils.checked_cast_to_sublime_value( + value, + ValueError("Post-mapped value is invalid (not a SublimeValue).") + ) + + if key.endswith("()"): res.instructions.append(ModelineInstruction_CallViewFunction(key[:-2], sublime_value)) + else: res.instructions.append(ModelineInstruction_SetViewSetting (key, sublime_value)) + except Exception as e: + Logger.warning(f"Failed converting modeline raw instruction to structured instruction. -- key=“{key}”, raw_value=“{raw_value}”, modifier=“{modifier}”, error=“{e}”") return res @abstractmethod From 305f53e3ba9b087a046bea3f9b58aecd99fc8a55 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Wed, 4 Mar 2026 23:52:19 +0200 Subject: [PATCH 106/128] Catch exception when parsing line for raw modeline instructions --- app/modeline_parser.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 56703b1..8d79113 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -29,8 +29,13 @@ def __init__(self): @final def parse_line(self, line: str) -> Optional[Modeline]: - instructions_raw = self.parse_line_raw(line) - if instructions_raw is None: + instructions_raw: Optional[List[Tuple[str, Optional[str], ModelineParser.ValueModifier]]] + try: + instructions_raw = self.parse_line_raw(line) + if instructions_raw is None: + return None + except Exception as e: + Logger.warning(f"Got an exception while parsing raw modeline instructions from a line. This is an error in the concrete subclass: it should return None instead. -- line=“{line}”, error=“{e}”") return None res = Modeline() From 33ae58e79a1a4cebfbe97f021a893af6a85b9963 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 00:05:20 +0200 Subject: [PATCH 107/128] Move ValueModifier to ModelineInstruction --- app/modeline_instruction.py | 6 ++++++ app/modeline_parser.py | 11 +++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/modeline_instruction.py b/app/modeline_instruction.py index 3a77f5d..f5e7a9f 100644 --- a/app/modeline_instruction.py +++ b/app/modeline_instruction.py @@ -1,11 +1,17 @@ from abc import ABC, abstractmethod +from enum import Enum import sublime class ModelineInstruction(ABC): + class ValueModifier(str, Enum): + NONE = "" + ADD = "+" + REMOVE = "-" + @abstractmethod def apply(self, view: sublime.View) -> None: pass diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 8d79113..62cc7f2 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -2,11 +2,11 @@ from typing import final, List, Optional, Tuple from abc import ABC, abstractmethod -from enum import Enum import json from .logger import Logger from .modeline import Modeline +from .modeline_instruction import ModelineInstruction from .modeline_instructions.set_view_setting import ModelineInstruction_SetViewSetting from .modeline_instructions.call_view_function import ModelineInstruction_CallViewFunction from .modeline_instructions_mapping import ModelineInstructionsMapping @@ -16,11 +16,6 @@ class ModelineParser(ABC): - class ValueModifier(str, Enum): - NONE = "" - ADD = "+" - REMOVE = "-" - def __init__(self): super().__init__() @@ -29,7 +24,7 @@ def __init__(self): @final def parse_line(self, line: str) -> Optional[Modeline]: - instructions_raw: Optional[List[Tuple[str, Optional[str], ModelineParser.ValueModifier]]] + instructions_raw: Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]] try: instructions_raw = self.parse_line_raw(line) if instructions_raw is None: @@ -74,7 +69,7 @@ def parse_line(self, line: str) -> Optional[Modeline]: return res @abstractmethod - def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ValueModifier]]]: + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: """ Abstract method whose concrete implementation should parse the given line as a dictionary of key/values if the line is a modeline. No parsing of any sort should be done on the key or value, including mappings; this will be handled by the `parse` function (which calls that function). From 2f8e56b2546fdf2b4daa390ed5c5cb56a7f06ea1 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 00:06:39 +0200 Subject: [PATCH 108/128] Store the value modifier in the modeline instructions that need it --- app/modeline_instruction.py | 9 +++++++-- app/modeline_instructions/call_view_function.py | 12 ++++++++---- app/modeline_instructions/set_view_setting.py | 10 ++++++---- app/modeline_parser.py | 4 ++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/modeline_instruction.py b/app/modeline_instruction.py index f5e7a9f..18e2289 100644 --- a/app/modeline_instruction.py +++ b/app/modeline_instruction.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod from enum import Enum -import sublime +from sublime import View as SublimeView +from sublime_types import Value as SublimeValue @@ -13,5 +14,9 @@ class ValueModifier(str, Enum): REMOVE = "-" @abstractmethod - def apply(self, view: sublime.View) -> None: + def __init__(self, key: str, value: SublimeValue, modifier: ValueModifier) -> None: + pass + + @abstractmethod + def apply(self, view: SublimeView) -> None: pass diff --git a/app/modeline_instructions/call_view_function.py b/app/modeline_instructions/call_view_function.py index 37cb73b..5931539 100644 --- a/app/modeline_instructions/call_view_function.py +++ b/app/modeline_instructions/call_view_function.py @@ -13,10 +13,14 @@ class ModelineInstruction_CallViewFunction(ModelineInstruction): function_name: str function_arg: SublimeValue - def __init__(self, function_name: str, function_arg: SublimeValue) -> None: - super().__init__() - self.function_name = function_name - self.function_arg = function_arg + def __init__(self, key: str, value: SublimeValue, modifier: ModelineInstruction.ValueModifier) -> None: + super().__init__(key, value, modifier) + + if modifier != ModelineInstruction.ValueModifier.NONE: + raise ValueError(f"Unsupported value modifier “{modifier}” for a call view function modeline instruction.") + + self.function_name = key + self.function_arg = value def apply(self, view: SublimeView) -> None: f = getattr(view, self.function_name) diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py index a81c0c9..5b33a0a 100644 --- a/app/modeline_instructions/set_view_setting.py +++ b/app/modeline_instructions/set_view_setting.py @@ -12,11 +12,13 @@ class ModelineInstruction_SetViewSetting(ModelineInstruction): setting_name: str setting_value: SublimeValue + setting_modifier: ModelineInstruction.ValueModifier - def __init__(self, setting_name: str, setting_value: SublimeValue) -> None: - super().__init__() - self.setting_name = setting_name - self.setting_value = setting_value + def __init__(self, key: str, value: SublimeValue, modifier: ModelineInstruction.ValueModifier) -> None: + super().__init__(key, value, modifier) + self.setting_name = key + self.setting_value = value + self.setting_modifier = modifier def apply(self, view: SublimeView) -> None: view.settings().set(self.setting_name, self.setting_value) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 62cc7f2..3738e8d 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -62,8 +62,8 @@ def parse_line(self, line: str) -> Optional[Modeline]: ValueError("Post-mapped value is invalid (not a SublimeValue).") ) - if key.endswith("()"): res.instructions.append(ModelineInstruction_CallViewFunction(key[:-2], sublime_value)) - else: res.instructions.append(ModelineInstruction_SetViewSetting (key, sublime_value)) + if key.endswith("()"): res.instructions.append(ModelineInstruction_CallViewFunction(key[:-2], sublime_value, modifier)) + else: res.instructions.append(ModelineInstruction_SetViewSetting (key, sublime_value, modifier)) except Exception as e: Logger.warning(f"Failed converting modeline raw instruction to structured instruction. -- key=“{key}”, raw_value=“{raw_value}”, modifier=“{modifier}”, error=“{e}”") return res From 09f2d5b5d6bf22eaa7af5001553e7c0f6b046eec Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 15:21:19 +0200 Subject: [PATCH 109/128] Add error log level to Logger --- app/logger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/logger.py b/app/logger.py index 22b60e9..97d9a2c 100644 --- a/app/logger.py +++ b/app/logger.py @@ -23,6 +23,10 @@ def info(s: str, *args) -> None: def warning(s: str, *args) -> None: Logger._log(Logger._format("*** ", s, *args)) + @staticmethod + def error(s: str, *args) -> None: + Logger._log(Logger._format("***** ERROR: ", s, *args)) + @staticmethod def _format(prefix: str, s: str, *args) -> str: return "[Sublime Modelines] " + prefix + (s % args) + "\n" From 5d2adec6609c80a772270b8ec7dedc012ce8baee Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 15:21:26 +0200 Subject: [PATCH 110/128] Dummy comment change --- app/modeline_instructions_mapping.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/modeline_instructions_mapping.py b/app/modeline_instructions_mapping.py index 8f47555..d512528 100644 --- a/app/modeline_instructions_mapping.py +++ b/app/modeline_instructions_mapping.py @@ -111,7 +111,8 @@ def __init__(self, raw_mapping_value: Dict[str, object]) -> None: raw_value_transform.get("parameters", {}), ValueError("Invalid “parameters” for a value transform: not a dictionary with string keys.") ) - # The match instruction has been added to Python 3.10 only. + # The “match” instruction has been added to Python 3.10. + # We use `if elif else` instead. type = Utils.checked_cast_to_optional_string(raw_value_transform.get("type")) if type == "lowercase": self.value_transforms.append(self.ValueTransformLowercase(params)) elif type == "map": self.value_transforms.append(self.ValueTransformMapping(params)) From ec2acc86452c77a36355c443b63c2ab499930614 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 15:22:00 +0200 Subject: [PATCH 111/128] =?UTF-8?q?Implement=20value=20modifier=20in=20?= =?UTF-8?q?=E2=80=9Cset=20view=20setting=E2=80=9D=20instruction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modeline_instructions/set_view_setting.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py index 5b33a0a..1f7650d 100644 --- a/app/modeline_instructions/set_view_setting.py +++ b/app/modeline_instructions/set_view_setting.py @@ -3,6 +3,7 @@ from sublime import View as SublimeView from sublime_types import Value as SublimeValue +from ..logger import Logger from ..modeline_instruction import ModelineInstruction @@ -21,4 +22,43 @@ def __init__(self, key: str, value: SublimeValue, modifier: ModelineInstruction. self.setting_modifier = modifier def apply(self, view: SublimeView) -> None: - view.settings().set(self.setting_name, self.setting_value) + settings = view.settings() + + new_setting_value: SublimeValue + # The “match” instruction has been added to Python 3.10. + # We use `if elif else` instead. + if self.setting_modifier == ModelineInstruction.ValueModifier.NONE: + new_setting_value = self.setting_value + + elif self.setting_modifier == ModelineInstruction.ValueModifier.ADD: + # We’re told to add the given value(s) to the current value. + # We can do this only if the current value is a list. + # (Technically we could probably imagine rules for strings, dictionaries, etc., but they would be a stretch; let’s stay simple.) + current_value = settings.get(self.setting_name, []) + if isinstance(current_value, list): + if isinstance(self.setting_value, list): new_setting_value = current_value + self.setting_value + else: new_setting_value = current_value + [self.setting_value] + else: + # If the current value is not a list, we fail. + # Note current_value should never be None as we ask for an empty list for the default value. + raise ValueError("Cannot add value to a non list setting.") + + elif self.setting_modifier == ModelineInstruction.ValueModifier.REMOVE: + # We’re told to remove the given value(s) to the current value. + # We can do this only if the current value is a list. + # (Technically we could probably imagine rules for strings, dictionaries, etc., but they would be a stretch; let’s stay simple.) + current_value = settings.get(self.setting_name) + if current_value is None: + new_setting_value = None + elif isinstance(current_value, list): + if isinstance(self.setting_value, list): new_setting_value = [v for v in current_value if not v in self.setting_value] + else: new_setting_value = [v for v in current_value if not v == self.setting_value] + else: + # If the current value is not a list, we fail. + raise ValueError("Cannot remove value to a non list setting.") + + else: + Logger.error(f"Unknown setting modifier “{self.setting_modifier}” when applying a `SetViewSetting` modeline instruction.") + raise Exception("Unknown setting modifier.") + + settings.set(self.setting_name, new_setting_value) From 658b43a786497e5d06dbfd3348821fec2c975dc6 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 16:52:43 +0200 Subject: [PATCH 112/128] Create the (unimplemented) modeline parsers --- app/modeline_parser.py | 5 +++-- app/modeline_parsers/emacs.py | 16 ++++++++++++++++ app/modeline_parsers/legacy.py | 16 ++++++++++++++++ app/modeline_parsers/sublime.py | 16 ++++++++++++++++ app/modeline_parsers/vim.py | 16 ++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 app/modeline_parsers/emacs.py create mode 100644 app/modeline_parsers/legacy.py create mode 100644 app/modeline_parsers/sublime.py create mode 100644 app/modeline_parsers/vim.py diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 3738e8d..7ac1c4d 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -56,7 +56,7 @@ def parse_line(self, line: str) -> Optional[Modeline]: (key, value) = key_value_pair # Apply the post-mapping transform on the key. - key = self.transform_key_post_mapping(key) + key = self.transform_key_postmapping(key) sublime_value = Utils.checked_cast_to_sublime_value( value, ValueError("Post-mapped value is invalid (not a SublimeValue).") @@ -77,7 +77,7 @@ def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], M """ pass - def transform_key_post_mapping(self, key: str) -> str: + def transform_key_postmapping(self, key: str) -> str: """ Gives an opportunity to concrete sub-classes to post-process the key after the mapping has been applied. This is used for instance by the VIM modeline parser class to implement Sublime commands with a prefix. @@ -86,6 +86,7 @@ def transform_key_post_mapping(self, key: str) -> str: # Parse strings that starts with either a double-quote (`"`), a brace (`{`) or a bracket (`[`) as a JSON string. + @final def __parse_jsonesque_str(self, str: str) -> object: if not str.startswith('"') and not str.startswith('{') and not str.startswith('['): return None diff --git a/app/modeline_parsers/emacs.py b/app/modeline_parsers/emacs.py new file mode 100644 index 0000000..3fa2697 --- /dev/null +++ b/app/modeline_parsers/emacs.py @@ -0,0 +1,16 @@ +from typing import final, List, Optional, Tuple + +from ..modeline_instruction import ModelineInstruction +from ..modeline_parser import ModelineParser + + + +@final +class ModelineParser_Emacs(ModelineParser): + + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: + raise Exception("Not Implemented") + + + def transform_key_postmapping(self, key: str) -> str: + return super().transform_key_postmapping(key) diff --git a/app/modeline_parsers/legacy.py b/app/modeline_parsers/legacy.py new file mode 100644 index 0000000..4a5ebef --- /dev/null +++ b/app/modeline_parsers/legacy.py @@ -0,0 +1,16 @@ +from typing import final, List, Optional, Tuple + +from ..modeline_instruction import ModelineInstruction +from ..modeline_parser import ModelineParser + + + +@final +class ModelineParser_Legacy(ModelineParser): + + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: + raise Exception("Not Implemented") + + + def transform_key_postmapping(self, key: str) -> str: + return super().transform_key_postmapping(key) diff --git a/app/modeline_parsers/sublime.py b/app/modeline_parsers/sublime.py new file mode 100644 index 0000000..23c5212 --- /dev/null +++ b/app/modeline_parsers/sublime.py @@ -0,0 +1,16 @@ +from typing import final, List, Optional, Tuple + +from ..modeline_instruction import ModelineInstruction +from ..modeline_parser import ModelineParser + + + +@final +class ModelineParser_Sublime(ModelineParser): + + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: + raise Exception("Not Implemented") + + + def transform_key_postmapping(self, key: str) -> str: + return super().transform_key_postmapping(key) diff --git a/app/modeline_parsers/vim.py b/app/modeline_parsers/vim.py new file mode 100644 index 0000000..361d2cb --- /dev/null +++ b/app/modeline_parsers/vim.py @@ -0,0 +1,16 @@ +from typing import final, List, Optional, Tuple + +from ..modeline_instruction import ModelineInstruction +from ..modeline_parser import ModelineParser + + + +@final +class ModelineParser_VIM(ModelineParser): + + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: + raise Exception("Not Implemented") + + + def transform_key_postmapping(self, key: str) -> str: + return super().transform_key_postmapping(key) From 67e582132d06885af2d166d909900f418b4d6a02 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 22:12:45 +0200 Subject: [PATCH 113/128] Implement the core loop for parsing and applying the modelines --- plugin.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index 77a4d7e..ba55e18 100644 --- a/plugin.py +++ b/plugin.py @@ -1,10 +1,16 @@ -from typing import Final, Optional +from typing import Final, List, Optional import sublime, sublime_plugin from .app.logger import Logger -from .app.settings import Settings from .app.logger_settings import updateLoggerSettings +from .app.modeline import Modeline +from .app.modeline_parser import ModelineParser +from .app.modeline_parsers.emacs import ModelineParser_Emacs +from .app.modeline_parsers.legacy import ModelineParser_Legacy +from .app.modeline_parsers.sublime import ModelineParser_Sublime +from .app.modeline_parsers.vim import ModelineParser_VIM +from .app.settings import ModelineFormat, Settings # The plugin structure is heavily inspired by . @@ -78,3 +84,52 @@ def do_modelines(view: sublime.View) -> None: Logger.debug("Searching for and applying modelines.") view.erase_status(PLUGIN_NAME) + + nstart = settings.number_of_lines_to_check_from_beginning() + nend = settings.number_of_lines_to_check_from_end() + lines: List[sublime.Region] = [] + if nstart > 0: + # Grab lines from beginning of view. + regionEnd = view.text_point(nstart, 0) + region = sublime.Region(0, regionEnd) + lines = view.lines(region) + if nend > 0: + # Get the last line in the file. + line = view.line(view.size()) + # Add the last N lines of the file to the lines list. + for i in range(0, nend): + # Add the line to the list of lines + lines.append(line) + # Move the line to the previous line + line = view.line(line.a - 1) + + parsers: List[ModelineParser] = [] + for parser_id in settings.modelines_formats(): + # The “match” instruction has been added to Python 3.10. + # We use `if elif else` instead. + if parser_id == ModelineFormat.DEFAULT: parsers.append(ModelineParser_Sublime()) + elif parser_id == ModelineFormat.VIM: parsers.append(ModelineParser_VIM()) + elif parser_id == ModelineFormat.EMACS: parsers.append(ModelineParser_Emacs()) + elif parser_id == ModelineFormat.LEGACY: parsers.append(ModelineParser_Legacy()) + else: raise Exception("Internal error: Unknown parser ID.") + + for line in lines: + line = view.substr(line) + for parser in parsers: + modeline: Optional[Modeline] + try: + modeline = parser.parse_line(line) + except Exception as e: + Logger.warning(f"Got exception while parsing line with parser “{type(parser)}”. Ignoring. (Note: This should not have happened!) exception=“{e}”, line=“{line}”") + continue + + if not modeline is None: + for instruction in modeline.instructions: + try: + instruction.apply(view) + except Exception as e: + Logger.warning(f"Got exception while applying modeline instruction. Ignoring. exception=“{e}”, line=“{line}”") + continue + + # We do not continue to the next parser. + break From fdd75ccbd68f24bde6f1980cd64112a8aae45b9b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 22:13:06 +0200 Subject: [PATCH 114/128] Fix apply_on_load and apply_on_save settings being ignored --- plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index ba55e18..a12da64 100644 --- a/plugin.py +++ b/plugin.py @@ -58,12 +58,12 @@ def __init__(self): def on_load(self, view: sublime.View) -> None: Logger.debug("on_load called.") - if settings.apply_on_load: + if settings.apply_on_load(): do_modelines(view) def on_post_save(self, view: sublime.View) -> None: Logger.debug("on_post_save called.") - if settings.apply_on_save: + if settings.apply_on_save(): do_modelines(view) From 0e4e07f05eee251165632b84ccd735623b6744a6 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 23:38:21 +0200 Subject: [PATCH 115/128] Fix checked_cast_to_sublime_value --- app/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/utils.py b/app/utils.py index a856425..6799d38 100644 --- a/app/utils.py +++ b/app/utils.py @@ -58,9 +58,12 @@ def checked_cast_to_dict_of_dict_with_string_keys(variable: object, exception: E @staticmethod def checked_cast_to_sublime_value(variable: object, exception: Exception = ValueError("Given object is not a Sublime Value.")) -> SublimeValue: """Casts the given object to a Sublime Value; raises the given exception if the given object is not that.""" + if variable is None: + return cast(SublimeValue, variable) # I don’t think there is a way to automatically check all the elements of the Value union, so we do them manually. # We’ll have to manually update the checks when the Value type is updated in Sublime. - for t in [bool, str, int, float, List[Any], Dict[str, Any], None]: + # Note: We do None separately because NoneType causes issues w/ Python 3.8 apparently. + for t in [bool, str, int, float, list, dict]: if isinstance(variable, t): return cast(SublimeValue, variable) raise exception From 67997f23fb6472b80e313a2d71d7c9ec48d74a7c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 23:38:43 +0200 Subject: [PATCH 116/128] Add more specs for the default modeline format --- Sublime Modelines.sublime-settings | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index d8d9c99..f0f2004 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -15,7 +15,7 @@ * Examples: * `// ~*~ sublime: key=val; key2=val2; key3 ~*~` * `// ~*~ sublime: key = val; key2+=val2; ~*~` - * `// ~*~ sublime: key=["hello": "world"] ~*~` + * `// ~*~ sublime : key=["hello": "world"] ~*~` * (Also works with /*-styled comments, but JSON does not supported nested comments, so I’m skipping this example…) * * This format is inspired by VIM (`sublime:` prefix, key=val) as well as Emacs (`~*~` delimiters; Emacs uses `-*-`). @@ -30,7 +30,9 @@ * * All values are trimmed of their spaces (before being parsed if the value is a JSON string). * If a value should contain a semicolon (`;`), it should be doubled (included inside a JSON string) - * to avoid being interpreted as the delimiter for the end of the value. */ + * to avoid being interpreted as the delimiter for the end of the value. + * + * To avoid ambiguities, if there are multiple `~*~` tokens on the line, only the first and last are considered. */ "default", /* Classic (legacy) format. From fb46f6bdda846a46fc7e002f8cd2921abb1ace38 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 23:38:58 +0200 Subject: [PATCH 117/128] Fix default Modeline not having any instructions --- app/modeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modeline.py b/app/modeline.py index cdb0174..da73cfe 100644 --- a/app/modeline.py +++ b/app/modeline.py @@ -7,7 +7,7 @@ class Modeline: - instructions: List[ModelineInstruction] + instructions: List[ModelineInstruction] = [] def __init__(self): super().__init__() From 4c20aeb782dc2ded8ff791744cb805b725167245 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Thu, 5 Mar 2026 23:47:16 +0200 Subject: [PATCH 118/128] Print the modeline instruction being applied before applying them --- app/modeline_instructions/call_view_function.py | 4 ++++ app/modeline_instructions/set_view_setting.py | 4 ++++ plugin.py | 1 + 3 files changed, 9 insertions(+) diff --git a/app/modeline_instructions/call_view_function.py b/app/modeline_instructions/call_view_function.py index 5931539..92e797c 100644 --- a/app/modeline_instructions/call_view_function.py +++ b/app/modeline_instructions/call_view_function.py @@ -25,3 +25,7 @@ def __init__(self, key: str, value: SublimeValue, modifier: ModelineInstruction. def apply(self, view: SublimeView) -> None: f = getattr(view, self.function_name) f(self.function_arg) + + + def __str__(self) -> str: + return f"ModelineInstruction: CallViewFunction: {self.function_name}()={self.function_arg}" diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py index 1f7650d..a5f951e 100644 --- a/app/modeline_instructions/set_view_setting.py +++ b/app/modeline_instructions/set_view_setting.py @@ -62,3 +62,7 @@ def apply(self, view: SublimeView) -> None: raise Exception("Unknown setting modifier.") settings.set(self.setting_name, new_setting_value) + + + def __str__(self) -> str: + return f"ModelineInstruction: SetViewSetting: {self.setting_name}{self.setting_modifier}={self.setting_value}" diff --git a/plugin.py b/plugin.py index a12da64..dfb2766 100644 --- a/plugin.py +++ b/plugin.py @@ -126,6 +126,7 @@ def do_modelines(view: sublime.View) -> None: if not modeline is None: for instruction in modeline.instructions: try: + Logger.debug(f"Applying modeline instruction: {instruction}.") instruction.apply(view) except Exception as e: Logger.warning(f"Got exception while applying modeline instruction. Ignoring. exception=“{e}”, line=“{line}”") From 2ff52f2813d974542ad9a46d1a39462b45b6f9c8 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 00:45:24 +0200 Subject: [PATCH 119/128] Implement ModelineParser_Sublime --- app/modeline_parsers/sublime.py | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/app/modeline_parsers/sublime.py b/app/modeline_parsers/sublime.py index 23c5212..2f86698 100644 --- a/app/modeline_parsers/sublime.py +++ b/app/modeline_parsers/sublime.py @@ -1,5 +1,7 @@ from typing import final, List, Optional, Tuple +import re + from ..modeline_instruction import ModelineInstruction from ..modeline_parser import ModelineParser @@ -9,8 +11,71 @@ class ModelineParser_Sublime(ModelineParser): def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: - raise Exception("Not Implemented") - + # Find the first and last `~*~` tokens in the line, if any. + start = line.find(self.__token) + if start == -1: return None + end = line.rfind(self.__token) + if end == start: return None + line = line[start+len(self.__token):end].strip() + + # Verify the string between the two tokens starts with `sublime`. + if not line.startswith(self.__prefix): return None + line = line[len(self.__prefix):].strip() + + if not line.startswith(":"): return None + line = line[1:].strip() + + def find_next_tuple() -> Optional[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]: + nonlocal line + + if len(line) == 0: + return None + + # Read line until the next `+=`, `-=` or `=`. + match = self.__re__plus_minus_equal.search(line) + if match is None: + key = line; line = "" + return (key, None, ModelineInstruction.ValueModifier.NONE) + + operator = line[match.start():match.end()] + modifer: ModelineInstruction.ValueModifier + if operator == "=": modifer = ModelineInstruction.ValueModifier.NONE + elif operator == "+=": modifer = ModelineInstruction.ValueModifier.ADD + elif operator == "-=": modifer = ModelineInstruction.ValueModifier.REMOVE + else: raise Exception("Internal error: Unknown operator.") + + key = line[:match.start()] + line = line[match.end():] + + value: str = "" + while idx := line.find(";") + 1: # +1: If not found, idx will be 0, and thus we will exit the loop. + idx -= 1 + value += line[:idx] + line = line[idx+1:] + if len(line) > 0 and line[0] == ";": + value += ";" + line = line[1:] + else: + break + else: + value += line + line = "" + + return (key, value, modifer) + + try: + res: List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]] = [] + while tuple := find_next_tuple(): + res.append(tuple) + return res + except ValueError: + return None def transform_key_postmapping(self, key: str) -> str: return super().transform_key_postmapping(key) + + __token = "~*~" + __prefix = "sublime" + + __re__plus_minus_equal = re.compile(r"=|\+=|-=") + From 1bbc0b87d7f16e334da5266fa61f4912a20817c9 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 00:45:36 +0200 Subject: [PATCH 120/128] Dummy comment change --- app/modeline_instructions/set_view_setting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py index a5f951e..4be5060 100644 --- a/app/modeline_instructions/set_view_setting.py +++ b/app/modeline_instructions/set_view_setting.py @@ -39,7 +39,7 @@ def apply(self, view: SublimeView) -> None: if isinstance(self.setting_value, list): new_setting_value = current_value + self.setting_value else: new_setting_value = current_value + [self.setting_value] else: - # If the current value is not a list, we fail. + # If the current value is not a known type, we fail. # Note current_value should never be None as we ask for an empty list for the default value. raise ValueError("Cannot add value to a non list setting.") @@ -54,7 +54,7 @@ def apply(self, view: SublimeView) -> None: if isinstance(self.setting_value, list): new_setting_value = [v for v in current_value if not v in self.setting_value] else: new_setting_value = [v for v in current_value if not v == self.setting_value] else: - # If the current value is not a list, we fail. + # If the current value is not a known type, we fail. raise ValueError("Cannot remove value to a non list setting.") else: From d1bb017694598e283b8f666699aa88ac7288d04c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 01:31:44 +0200 Subject: [PATCH 121/128] Fix Modeline having its instructions saved in a class variable instead of an instance one --- app/modeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/modeline.py b/app/modeline.py index da73cfe..f7a19bd 100644 --- a/app/modeline.py +++ b/app/modeline.py @@ -7,7 +7,8 @@ class Modeline: - instructions: List[ModelineInstruction] = [] + instructions: List[ModelineInstruction] def __init__(self): super().__init__() + self.instructions = [] From 54c3d146c16d1c1303956a0ac6965ab6df2ca6bc Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 01:43:14 +0200 Subject: [PATCH 122/128] Dummy white-line removal --- app/modeline_parsers/sublime.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/modeline_parsers/sublime.py b/app/modeline_parsers/sublime.py index 2f86698..082132d 100644 --- a/app/modeline_parsers/sublime.py +++ b/app/modeline_parsers/sublime.py @@ -78,4 +78,3 @@ def transform_key_postmapping(self, key: str) -> str: __prefix = "sublime" __re__plus_minus_equal = re.compile(r"=|\+=|-=") - From 479a16082e415925e85d5b110ff6189aa20f3c3c Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 15:57:50 +0200 Subject: [PATCH 123/128] Do not process overlapping lines twice --- plugin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin.py b/plugin.py index dfb2766..014c306 100644 --- a/plugin.py +++ b/plugin.py @@ -93,6 +93,7 @@ def do_modelines(view: sublime.View) -> None: regionEnd = view.text_point(nstart, 0) region = sublime.Region(0, regionEnd) lines = view.lines(region) + last_first_lines = lines[-1] if len(lines) > 0 else None if nend > 0: # Get the last line in the file. line = view.line(view.size()) @@ -100,8 +101,14 @@ def do_modelines(view: sublime.View) -> None: for i in range(0, nend): # Add the line to the list of lines lines.append(line) + if line.a == 0: + # We are at the first line; let’s stop there. + break # Move the line to the previous line line = view.line(line.a - 1) + if not last_first_lines is None and line.a < last_first_lines.b: + # No overlapping lines. + break parsers: List[ModelineParser] = [] for parser_id in settings.modelines_formats(): From cd3182ee5b56190052a824c3aa9e542eae91e09b Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 23:09:33 +0200 Subject: [PATCH 124/128] Dummy comment enhancement --- app/modeline_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/modeline_parser.py b/app/modeline_parser.py index 7ac1c4d..f1ddeb8 100644 --- a/app/modeline_parser.py +++ b/app/modeline_parser.py @@ -80,7 +80,8 @@ def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], M def transform_key_postmapping(self, key: str) -> str: """ Gives an opportunity to concrete sub-classes to post-process the key after the mapping has been applied. - This is used for instance by the VIM modeline parser class to implement Sublime commands with a prefix. + This is used for instance by the VIM modeline parser class to implement Sublime commands with a prefix, bypassing the mapping. + In practice this is very much useless and only there for full backward compatibility. """ return key From ed1e50c687be52ddffc8e4c5d4fbef933bf32958 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 23:09:50 +0200 Subject: [PATCH 125/128] Fix VIM and Emacs mapping for syntax --- Sublime Modelines.sublime-settings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index f0f2004..69e1f56 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -82,7 +82,7 @@ /* Set line endings (DOS, Legacy MacOS, UNIX). */ "fileformat": {"aliases": ["ff"], "key": "set_line_endings()", "value-mapping": {"dos": "windows", "mac": "CR", "unix": "unix"}}, /* Set the syntax of the file. */ - "filetype": {"aliases": ["ft"], "key": "x_syntax"}, + "filetype": {"aliases": ["ft"], "key": "syntax"}, /* # of columns for each tab character. */ "tabstop": {"aliases": ["ts"], "key": "tab_size"}, /* # of columns for indent operation. */ @@ -177,7 +177,7 @@ /* Tabs → Spaces enable/disable. */ "indent-tabs-mode": {"key": "translate_tabs_to_spaces", "value-mapping": {"nil": true, "0": true}, "value-mapping-default": false}, /* Set the syntax of the file. */ - "mode": {"key": "x_syntax"}, + "mode": {"key": "syntax"}, /* # of columns for each tab character. */ "tab-width": {"key": "tab_size"}, }, From bc9787dce6d02c32f04bdc02e6a9580dd65388a7 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 23:10:12 +0200 Subject: [PATCH 126/128] Implement Emacs parser --- app/modeline_parsers/emacs.py | 37 +++++++++++++++++++++++++++++++++-- plugin.py | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/modeline_parsers/emacs.py b/app/modeline_parsers/emacs.py index 3fa2697..dd3d41c 100644 --- a/app/modeline_parsers/emacs.py +++ b/app/modeline_parsers/emacs.py @@ -1,6 +1,9 @@ from typing import final, List, Optional, Tuple +import re + from ..modeline_instruction import ModelineInstruction +from ..modeline_instructions_mapping import ModelineInstructionsMapping from ..modeline_parser import ModelineParser @@ -8,9 +11,39 @@ @final class ModelineParser_Emacs(ModelineParser): + def __init__(self, mapping: ModelineInstructionsMapping): + super().__init__() + self.mapping = mapping + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: - raise Exception("Not Implemented") + # From . + # We probably should rewrite this properly though… + m = re.match(self.__modeline_re, line) + if not m: return None + + res: List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]] = [] + + modeline = m.group(1) # Original implementation had a lowercase here. It does not make sense though. + for opt in modeline.split(";"): + opt = opt.strip() + if len(opt) == 0: continue + + opts = re.match(r"\s*(st-|sublime-text-|sublime-|sublimetext-)?(.+):\s*(.+)\s*", opt) + if opts: + key, value = (self.__sublime_prefix if opts.group(1) else "") + opts.group(2), opts.group(3) + res.append((key, value, ModelineInstruction.ValueModifier.NONE)) + else: + # Not a `key: value`-pair: we assume it’s a syntax-name. + res.append(("syntax", opt.strip(), ModelineInstruction.ValueModifier.NONE)) + + return res def transform_key_postmapping(self, key: str) -> str: - return super().transform_key_postmapping(key) + transformed = super().transform_key_postmapping(key) + if transformed.startswith(self.__sublime_prefix): + transformed = transformed[len(self.__sublime_prefix):] + return transformed + + __modeline_re = r".*-\*-\s*(.+?)\s*-\*-.*" + __sublime_prefix = "sublimetext--" diff --git a/plugin.py b/plugin.py index 014c306..e1f8e53 100644 --- a/plugin.py +++ b/plugin.py @@ -116,7 +116,7 @@ def do_modelines(view: sublime.View) -> None: # We use `if elif else` instead. if parser_id == ModelineFormat.DEFAULT: parsers.append(ModelineParser_Sublime()) elif parser_id == ModelineFormat.VIM: parsers.append(ModelineParser_VIM()) - elif parser_id == ModelineFormat.EMACS: parsers.append(ModelineParser_Emacs()) + elif parser_id == ModelineFormat.EMACS: parsers.append(ModelineParser_Emacs(settings.emacsMapping())) elif parser_id == ModelineFormat.LEGACY: parsers.append(ModelineParser_Legacy()) else: raise Exception("Internal error: Unknown parser ID.") From 743da8b79aead577f97774f05ee067375c87a482 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 23:38:20 +0200 Subject: [PATCH 127/128] Implement automatic finding of syntax file for the syntax key --- app/modeline_instructions/set_view_setting.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/modeline_instructions/set_view_setting.py b/app/modeline_instructions/set_view_setting.py index 4be5060..036d091 100644 --- a/app/modeline_instructions/set_view_setting.py +++ b/app/modeline_instructions/set_view_setting.py @@ -1,7 +1,9 @@ from typing import final +from os import path from sublime import View as SublimeView from sublime_types import Value as SublimeValue +import sublime from ..logger import Logger from ..modeline_instruction import ModelineInstruction @@ -24,6 +26,24 @@ def __init__(self, key: str, value: SublimeValue, modifier: ModelineInstruction. def apply(self, view: SublimeView) -> None: settings = view.settings() + # Process setting value for special `syntax` case. + # Note might be a better algorithm. + # Among other things, it allows users to have a custom mapping of syntaxes, which we don’t. + if (self.setting_name == "syntax" and + isinstance(self.setting_value, str) and + not self.setting_value.endswith("tmLanguage") and + not self.setting_value.endswith("sublime-syntax") and + not "/" in self.setting_value and + hasattr(sublime, "find_resources") + ): + # We modify the value to find the proper file (this avoids specifying `Swift.tmLanguage`; instead we can use `Swift`). + candidates = sublime.find_resources(f"{self.setting_value}.sublime-syntax") + sublime.find_resources(f"{self.setting_value}.tmLanguage") + if len(candidates) > 0: + # Note: We only use the basename of the found resource. + # For some (unknown) reason, using the full path and the basename does not yield the same results, + # even when there is only one possible alternative! + self.setting_value = path.basename(path.normpath(candidates[0])) + new_setting_value: SublimeValue # The “match” instruction has been added to Python 3.10. # We use `if elif else` instead. From fc3153d3a3860626eebd4cec00f24d605be9e6c6 Mon Sep 17 00:00:00 2001 From: Frizlab Date: Fri, 6 Mar 2026 23:43:39 +0200 Subject: [PATCH 128/128] Add classic+vim parser --- Sublime Modelines.sublime-settings | 29 +++++++++++++++++------------ app/modeline_parsers/emacs.py | 1 + app/modeline_parsers/legacy_vim.py | 16 ++++++++++++++++ app/settings.py | 9 +++++---- plugin.py | 10 ++++++---- 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 app/modeline_parsers/legacy_vim.py diff --git a/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings index 69e1f56..ab96047 100644 --- a/Sublime Modelines.sublime-settings +++ b/Sublime Modelines.sublime-settings @@ -35,18 +35,6 @@ * To avoid ambiguities, if there are multiple `~*~` tokens on the line, only the first and last are considered. */ "default", - /* Classic (legacy) format. - * Example: `# sublime: key val(; key2 val2)*` - * - * Usually works well unless putting the modeline in a `/*`-style comment. - * - * Can also not work when the syntax of the file is not known, - * because we check the line to begin with the comment char before parsing it - * (`#` is used when the character is unknown). - * - * The parsing algorithm is exactly the same as the old ST2 version of the plugin. */ - //"classic", - /* VIM-like modelines. * Examples (straight from ): * - `// vim: noai:ts=4:sw=4` @@ -70,6 +58,23 @@ * * Just like for the VIM format, we map the Emacs commands to Sublime Text commands. */ //"emacs", + + /* Classic (legacy) format. + * Example: `# sublime: key val(; key2 val2)*` + * + * Usually works well unless putting the modeline in a `/*`-style comment. + * + * Can also not work when the syntax of the file is not known, + * because we check the line to begin with the comment char before parsing it + * (`#` is used when the character is unknown). + * + * The parsing algorithm is exactly the same as the old ST2 version of the plugin. */ + //"classic", + + /* Classic (legacy) format with VIM support. + * + * Same as previous, with original VIM support implementation. */ + //"classic+vim", ], /* Default VIM commands mapping. diff --git a/app/modeline_parsers/emacs.py b/app/modeline_parsers/emacs.py index dd3d41c..f86c70c 100644 --- a/app/modeline_parsers/emacs.py +++ b/app/modeline_parsers/emacs.py @@ -15,6 +15,7 @@ def __init__(self, mapping: ModelineInstructionsMapping): super().__init__() self.mapping = mapping + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: # From . # We probably should rewrite this properly though… diff --git a/app/modeline_parsers/legacy_vim.py b/app/modeline_parsers/legacy_vim.py new file mode 100644 index 0000000..ee30af8 --- /dev/null +++ b/app/modeline_parsers/legacy_vim.py @@ -0,0 +1,16 @@ +from typing import final, List, Optional, Tuple + +from ..modeline_instruction import ModelineInstruction +from ..modeline_parser import ModelineParser + + + +@final +class ModelineParser_LegacyVIM(ModelineParser): + + def parse_line_raw(self, line: str) -> Optional[List[Tuple[str, Optional[str], ModelineInstruction.ValueModifier]]]: + raise Exception("Not Implemented") + + + def transform_key_postmapping(self, key: str) -> str: + return super().transform_key_postmapping(key) diff --git a/app/settings.py b/app/settings.py index b380c83..b88eb0c 100644 --- a/app/settings.py +++ b/app/settings.py @@ -11,10 +11,11 @@ class ModelineFormat(str, Enum): - DEFAULT = "default" - LEGACY = "classic" - VIM = "vim" - EMACS = "emacs" + DEFAULT = "default" + VIM = "vim" + EMACS = "emacs" + LEGACY = "classic" + LEGACY_VIM = "classic+vim" class Settings: diff --git a/plugin.py b/plugin.py index e1f8e53..de93393 100644 --- a/plugin.py +++ b/plugin.py @@ -8,6 +8,7 @@ from .app.modeline_parser import ModelineParser from .app.modeline_parsers.emacs import ModelineParser_Emacs from .app.modeline_parsers.legacy import ModelineParser_Legacy +from .app.modeline_parsers.legacy_vim import ModelineParser_LegacyVIM from .app.modeline_parsers.sublime import ModelineParser_Sublime from .app.modeline_parsers.vim import ModelineParser_VIM from .app.settings import ModelineFormat, Settings @@ -114,10 +115,11 @@ def do_modelines(view: sublime.View) -> None: for parser_id in settings.modelines_formats(): # The “match” instruction has been added to Python 3.10. # We use `if elif else` instead. - if parser_id == ModelineFormat.DEFAULT: parsers.append(ModelineParser_Sublime()) - elif parser_id == ModelineFormat.VIM: parsers.append(ModelineParser_VIM()) - elif parser_id == ModelineFormat.EMACS: parsers.append(ModelineParser_Emacs(settings.emacsMapping())) - elif parser_id == ModelineFormat.LEGACY: parsers.append(ModelineParser_Legacy()) + if parser_id == ModelineFormat.DEFAULT: parsers.append(ModelineParser_Sublime()) + elif parser_id == ModelineFormat.VIM: parsers.append(ModelineParser_VIM()) + elif parser_id == ModelineFormat.EMACS: parsers.append(ModelineParser_Emacs(settings.emacsMapping())) + elif parser_id == ModelineFormat.LEGACY: parsers.append(ModelineParser_Legacy()) + elif parser_id == ModelineFormat.LEGACY_VIM: parsers.append(ModelineParser_LegacyVIM()) else: raise Exception("Internal error: Unknown parser ID.") for line in lines: