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 01/69] 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 02/69] 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 03/69] 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 04/69] 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 05/69] 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 06/69] 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 07/69] 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 08/69] 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 09/69] 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 10/69] 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 11/69] =?UTF-8?q?Remove=20obsolete=20=E2=80=9Cmake=20relea?= =?UTF-8?q?se=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 12/69] 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 13/69] 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 14/69] 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 15/69] 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 16/69] 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 17/69] 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 18/69] 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 19/69] 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 20/69] 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 21/69] 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 22/69] 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 23/69] 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 24/69] 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 25/69] 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 26/69] 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 27/69] 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 28/69] 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 29/69] 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 30/69] 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 31/69] 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 32/69] 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 33/69] 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 34/69] 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 35/69] 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 36/69] 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 37/69] 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 38/69] 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 39/69] 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 40/69] 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 41/69] 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 42/69] 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 43/69] 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 44/69] 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 45/69] 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 46/69] 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 47/69] 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 48/69] 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 49/69] =?UTF-8?q?Add=20Guillermo=20L=C3=B3pez-Anglada=20co?= =?UTF-8?q?ntribution=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 50/69] =?UTF-8?q?Add=20Guillermo=20L=C3=B3pez-Anglada=20Co?= =?UTF-8?q?pyright=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 51/69] 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 52/69] 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 53/69] 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 54/69] 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 55/69] 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 56/69] 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 57/69] 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 58/69] 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 59/69] 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 60/69] 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 61/69] 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 62/69] 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 63/69] 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 64/69] 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 65/69] 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 66/69] 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 67/69] 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 68/69] =?UTF-8?q?Add=20all=20the=20=E2=80=9Cspecs=E2=80=9D?= =?UTF-8?q?=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 69/69] 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: