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 diff --git a/.gitignore b/.gitignore index a556819..7a1422d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,11 @@ -*.hgignore -*.hgtags -*.pyc -*.cache -*.sublime-project - -_*.txt -sample-grammar.js -Manifest -MANIFEST - -dist/ -build/ \ No newline at end of file +*.pyc +*.cache +*.sublime-project + +_*.txt +sample-grammar.js +Manifest +MANIFEST + +dist/ +build/ 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 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/LICENSE.TXT b/License.txt similarity index 96% rename from LICENSE.TXT rename to License.txt index 021ec65..b02fa06 100644 --- a/LICENSE.TXT +++ b/License.txt @@ -1,19 +1,20 @@ -Copyright (c) 2010 Guillermo López-Anglada - -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. \ No newline at end of file +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 +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 deleted file mode 100644 index fa6606a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include sublime_modelines.py -include LICENSE.TXT -include README.rst -prune setup.py \ No newline at end of file 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/README.rst b/README.rst deleted file mode 100644 index 0d788ad..0000000 --- a/README.rst +++ /dev/null @@ -1,72 +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. - -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. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..3090881 --- /dev/null +++ b/Readme.md @@ -0,0 +1,90 @@ +# 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]() 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 +# sublime: x_syntax Foo +or +# sublime: x_syntax Packages/Foo/Foo.tmLanguage +``` + +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; +- 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/Sublime Modelines.sublime-settings b/Sublime Modelines.sublime-settings new file mode 100644 index 0000000..784696b --- /dev/null +++ b/Sublime Modelines.sublime-settings @@ -0,0 +1,187 @@ +{ + + /* 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": [ + + /* 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). + * + * 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", + + ], + + /* 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, + /* 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/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..4e2fae9 --- /dev/null +++ b/app/logger.py @@ -0,0 +1,37 @@ +import sys + + + +class Logger: + """A simple logger.""" + + enable_debug_log = False + log_to_tmp = False + + @staticmethod + def debug(s: str, *args) -> None: + if not Logger.enable_debug_log: + return + Logger._log(Logger._format("", s, *args)) + + @staticmethod + def info(s: str, *args) -> None: + Logger._log(Logger._format("", s, *args)) + + @staticmethod + def warning(s: str, *args) -> None: + Logger._log(Logger._format("*** ", s, *args)) + + @staticmethod + def _format(prefix: str, s: str, *args) -> str: + return "[Sublime Modelines] " + prefix + (s % args) + "\n" + + @staticmethod + def _log(str: str) -> None: + if Logger.log_to_tmp: + 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 new file mode 100644 index 0000000..2ae85d0 --- /dev/null +++ b/app/settings.py @@ -0,0 +1,76 @@ +# This can be removed when using Python >= 3.10. +from typing import List + +from enum import Enum +import sublime + +from .logger import Logger + + + +class ModelineFormat(str, Enum): + DEFAULT = "default" + LEGACY = "classic" + VIM = "vim" + EMACS = "emacs" + + +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) -> List[ModelineFormat]: + default_for_syntax_error = [ModelineFormat.DEFAULT] + + 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 + + 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) -> 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.") + 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/app/sublime_modelines.py b/app/sublime_modelines.py new file mode 100644 index 0000000..6267e36 --- /dev/null +++ b/app/sublime_modelines.py @@ -0,0 +1,242 @@ +# import re, sys, json, os + + +# debug_log("Modelines plugin start.") + + +# 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):(.*):.*$") + +# 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 + +# ST3 = sublime.version() >= "3000" + +# 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 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 = 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 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: +# yield m.groups() +# s = s[m.end():] +# continue + +# m = KEY_ONLY.match(s) +# if m: +# k, = m.groups() +# value = "true" + +# 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 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)) + +# 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 + +# 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 do_modelines(self, view): +# if not self._modes: +# self.init_syntax_files() + +# 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): +# 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 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: +# 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) + +# 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("\\", "/") 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/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 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 diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..726fdbd --- /dev/null +++ b/plugin.py @@ -0,0 +1,21 @@ +import sublime, sublime_plugin + +from .app.logger import Logger +from .app.settings import Settings + + + +class SublimeModelinesPlugin(sublime_plugin.EventListener): + + def __init__(self): + super().__init__() + Logger.updateSettings() + Logger.debug("Plugin init.") + + def on_load(self, view): + Logger.debug("on_load called.") + #self.do_modelines(view) + + def on_post_save(self, view): + Logger.debug("on_post_save called.") + #self.do_modelines(view) 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", + ] +} diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh new file mode 100755 index 0000000..6d0d7fd --- /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 "__pycache__" -o -name "build" -o -name "dist" \) -exec rm -frv {} + diff --git a/setup.py b/setup.py deleted file mode 100644 index d204cc0..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 diff --git a/sublime_modelines.py b/sublime_modelines.py deleted file mode 100644 index 7dc84e4..0000000 --- a/sublime_modelines.py +++ /dev/null @@ -1,119 +0,0 @@ -import sublime, sublime_plugin - -import re - - -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 - - -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))[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 gen_raw_options(modelines): - for m in modelines: - 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): - name, sep, value = opt.partition(' ') - yield view.settings().set, name.rstrip(':'), value.rstrip(';') - - -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. - """ - 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.") - - -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): - for setter, name, value in gen_modeline_options(view): - if name == 'x_syntax': - view.set_syntax_file(value) - else: - try: - setter(name, to_json_type(value)) - except ValueError, 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." - - 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 diff --git a/tests/sublime.py b/tests/sublime.py deleted file mode 100644 index 53f04ec..0000000 --- a/tests/sublime.py +++ /dev/null @@ -1,19 +0,0 @@ -# -#class View(object): -# pass -# -# -#class RegionSet(object): -# pass -# -# -#class Region(object): -# pass -# -# -#class Window(object): -# pass -# -# -#class Options(object): -# pass \ No newline at end of file diff --git a/tests/sublime_plugin.py b/tests/sublime_plugin.py deleted file mode 100644 index 4e09c54..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 \ No newline at end of file diff --git a/tests/test_modelines.py b/tests/test_modelines.py new file mode 100644 index 0000000..e732a84 --- /dev/null +++ b/tests/test_modelines.py @@ -0,0 +1,86 @@ +# This is the test file that was added with ST 3 compatibility. + +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): + 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 + + # 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) + + 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): + self._gen_raw_options_test( + '# sublime: set color_scheme="Packages/Color Scheme - Default/Monokai.tmTheme":', + [("color_scheme", "=", '"Packages/Color Scheme - Default/Monokai.tmTheme"')] + ) diff --git a/tests/test_sublime_modelines.py b/tests/test_sublime_modelines.py index 536f1fd..a5c63f1 100644 --- a/tests/test_sublime_modelines.py +++ b/tests/test_sublime_modelines.py @@ -1,157 +1,110 @@ -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 +# 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 plugin + + + +class SublimeModelinesTest(TestCase): + + # 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! + def test_get_line_comment_char_does_not_call_meta_info(self): + view = Mock() + #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() + +# 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", +# "# 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 + +# 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) +# self.assertEqual(sublime_modelines.to_json_type(d), True) +# self.assertEqual(sublime_modelines.to_json_type(e), e)