├── .github └── workflows │ └── codesee-arch-diagram.yml ├── .gitignore ├── Default.sublime-commands ├── LICENSE ├── Main.sublime-menu ├── README.md ├── SessionManager.py ├── SessionManager.sublime-settings ├── json ├── decoder.py └── encoder.py ├── messages.json ├── messages └── welcome ├── modules ├── messages.py ├── serialize.py ├── session.py ├── settings.py └── st_utils.py └── session_manager.sublime-project /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request_target: 6 | types: [opened, synchronize, reopened] 7 | 8 | name: CodeSee Map 9 | 10 | jobs: 11 | test_map_action: 12 | runs-on: ubuntu-latest 13 | continue-on-error: true 14 | name: Run CodeSee Map Analysis 15 | steps: 16 | - name: checkout 17 | id: checkout 18 | uses: actions/checkout@v2 19 | with: 20 | repository: ${{ github.event.pull_request.head.repo.full_name }} 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | fetch-depth: 0 23 | 24 | # codesee-detect-languages has an output with id languages. 25 | - name: Detect Languages 26 | id: detect-languages 27 | uses: Codesee-io/codesee-detect-languages-action@latest 28 | 29 | - name: Configure JDK 16 30 | uses: actions/setup-java@v2 31 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }} 32 | with: 33 | java-version: '16' 34 | distribution: 'zulu' 35 | 36 | # CodeSee Maps Go support uses a static binary so there's no setup step required. 37 | 38 | - name: Configure Node.js 14 39 | uses: actions/setup-node@v2 40 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} 41 | with: 42 | node-version: '14' 43 | 44 | - name: Configure Python 3.x 45 | uses: actions/setup-python@v2 46 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }} 47 | with: 48 | python-version: '3.10' 49 | architecture: 'x64' 50 | 51 | - name: Configure Ruby '3.x' 52 | uses: ruby/setup-ruby@v1 53 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }} 54 | with: 55 | ruby-version: '3.0' 56 | 57 | # CodeSee Maps Rust support uses a static binary so there's no setup step required. 58 | 59 | - name: Generate Map 60 | id: generate-map 61 | uses: Codesee-io/codesee-map-action@latest 62 | with: 63 | step: map 64 | github_ref: ${{ github.ref }} 65 | languages: ${{ steps.detect-languages.outputs.languages }} 66 | 67 | - name: Upload Map 68 | id: upload-map 69 | uses: Codesee-io/codesee-map-action@latest 70 | with: 71 | step: mapUpload 72 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 73 | github_ref: ${{ github.ref }} 74 | 75 | - name: Insights 76 | id: insights 77 | uses: Codesee-io/codesee-map-action@latest 78 | with: 79 | step: insights 80 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 81 | github_ref: ${{ github.ref }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Sublime 57 | *.sublime-workspace 58 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Session Manager: Save Session", 4 | "command": "save_session" 5 | }, 6 | { 7 | "caption": "Session Manager: Load Session", 8 | "command": "load_session" 9 | }, 10 | { 11 | "caption": "Session Manager: Delete Session", 12 | "command": "delete_session" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sascha Wolf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": 5 | [ 6 | { 7 | "caption": "Package Settings", 8 | "mnemonic": "P", 9 | "id": "package-settings", 10 | "children": 11 | [ 12 | { 13 | "caption": "Session Manager", 14 | "children": 15 | [ 16 | { 17 | "command": "open_file", 18 | "args": { 19 | "file": "${packages}/Session Manager/README.md", 20 | "platform": "Windows" 21 | }, 22 | "caption": "Help" 23 | }, 24 | { 25 | "command": "open_file", 26 | "args": { 27 | "file": "${packages}/Session Manager/README.md", 28 | "platform": "OSX" 29 | }, 30 | "caption": "Help" 31 | }, 32 | { 33 | "command": "open_file", 34 | "args": { 35 | "file": "${packages}/Session Manager/README.md", 36 | "platform": "Linux" 37 | }, 38 | "caption": "Help" 39 | }, 40 | { "caption": "-" }, 41 | { 42 | "command": "open_file", 43 | "args": { 44 | "file": "${packages}/Session Manager/SessionManager.sublime-settings", 45 | "platform": "Windows" 46 | }, 47 | "caption": "Settings - Default" 48 | }, 49 | { 50 | "command": "open_file", 51 | "args": { 52 | "file": "${packages}/Session Manager/SessionManager.sublime-settings", 53 | "platform": "OSX" 54 | }, 55 | "caption": "Settings - Default" 56 | }, 57 | { 58 | "command": "open_file", 59 | "args": { 60 | "file": "${packages}/Session Manager/SessionManager.sublime-settings", 61 | "platform": "Linux" 62 | }, 63 | "caption": "Settings - Default" 64 | }, 65 | { 66 | "command": "open_file", 67 | "args": { 68 | "file": "${packages}/User/SessionManager.sublime-settings", 69 | "platform": "Windows" 70 | }, 71 | "caption": "Settings - User" 72 | }, 73 | { 74 | "command": "open_file", 75 | "args": { 76 | "file": "${packages}/User/SessionManager.sublime-settings", 77 | "platform": "OSX" 78 | }, 79 | "caption": "Settings - User" 80 | }, 81 | { 82 | "command": "open_file", 83 | "args": { 84 | "file": "${packages}/User/SessionManager.sublime-settings", 85 | "platform": "Linux" 86 | }, 87 | "caption": "Settings - User" 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sublime-SessionManager 2 | ====================== 3 | 4 | Why? 5 | ---- 6 | 7 | Let's be honest: Sublime Text's session management is weird. It doesn't outright suck, but it's inflexible and hard to use if you don't have a project file. 8 | 9 | As long as you work in a single project everything is fine, but as soon as you want have multiple projects open at once, or just want to open a bunch of files and hack away, the sublime's session management screws you over. 10 | 11 | Sometimes you simply want to save your current state, do something different, and come back to wherever you left. Preferably quick and uncomplicated. 12 | 13 | If you've ever been in such a situation, then this plugin is for you. 14 | 15 | Installation 16 | ------------ 17 | 18 | The easiest way to install Session Manager is obviously through Sublime Package Control. Open `Package Control` (`Preferences` > `Package Control`), select `Package Control: Install package`, and search for "Session Manager". 19 | 20 | Alternatively, you can install the plugin via git or simply download the repository. 21 | 22 | For this you have to navigate to your package folder (use `Preferences` > `Browse Packages...` for this) and clone or unpack this repository in there. 23 | 24 | How to use 25 | ---------- 26 | 27 | Using Session Manager you can *save*, *load* and *delete* your current state. It handles all currently opened __windows__, __folders__ and __files__, down to the currently visible __region__ and all __selections__ and __cursors__. 28 | 29 | When you load a session you can continue your work as if nothing ever happened. 30 | 31 | The commands, accessible through the command palette (`ctrl+shift+p`), are: 32 | 33 | Session Manager: Save Session 34 | Session Manager: Load Session 35 | Session Manager: Delete Session 36 | 37 | The default name for a session consists of the keyword __session__ and the __current timestamp__ (example: `session_15-03-13T15-37-22`). If you don't like the format, you can configure it with the `session_name_format` setting. 38 | 39 | Each of your sessions will be saved in a `Packages/User/sessions` folder. This can be changed via the `session_path` setting. 40 | 41 | The sessions are simple JSON files; this means you can edit and change them as you see fit. 42 | 43 | You can also bind the commands to the keyboard: 44 | 45 | Session Manager: Save Session is save_session, 46 | Session Manager: Load Session is load_session, and 47 | Session Manager: Delete Session is delete_session. 48 | 49 | Configuration 50 | ------------- 51 | 52 | Just take a look at the default configuration file to learn about the available options: 53 | 54 | ```js 55 | { 56 | // If session_path is null, the sessions will be saved in your sublime User folder in sessions 57 | // (User/sessions) 58 | "session_path": null, 59 | 60 | // The format which shall be used to generate the default session name; 61 | // Example result: session_15-02-07T15-09-32 62 | // take a look at the python docs for details: 63 | // https://docs.python.org/3.3/library/datetime.html#strftime-strptime-behavior 64 | "session_name_format": "session_%y-%m-%dT%H-%M-%S" 65 | } 66 | ``` 67 | 68 | 69 | Plans for the future 70 | -------------------- 71 | 72 | These are the features I'm planning to add in the future: 73 | 74 | - Save only current window 75 | - Close everything and open a new window after saving (*configurable or another command?*) 76 | - More information when loading sessions (*so we can easier distinguish between them*) 77 | 78 | If you think I should add some other features, then don't hesitate to open an issue. 79 | 80 | __Happy session saving!__ 81 | -------------------------------------------------------------------------------- /SessionManager.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | from datetime import datetime 5 | 6 | from .modules import messages 7 | from .modules import serialize 8 | from .modules import settings 9 | from .modules.session import Session 10 | 11 | on_query_completions_callbacks = {} 12 | 13 | def plugin_loaded(): 14 | settings.load() 15 | 16 | 17 | def error_message(error_key, *args): 18 | sublime.error_message(messages.error(error_key, *args)) 19 | 20 | 21 | def get_sessions_list(): 22 | return serialize.available() 23 | 24 | 25 | class InputCompletionsListener(sublime_plugin.EventListener): 26 | def on_query_completions(self, view, prefix, locations): 27 | if view.id() in on_query_completions_callbacks.keys(): 28 | return on_query_completions_callbacks[view.id()](prefix, locations) 29 | 30 | 31 | class SaveSession(sublime_plugin.ApplicationCommand): 32 | input_panel = None 33 | 34 | def run(self): 35 | self.input_panel = sublime.active_window().show_input_panel( 36 | messages.dialog("session_name"), 37 | self.generate_name(), 38 | on_done=self.save_session, 39 | on_change=self.input_changed, 40 | on_cancel=None 41 | ) 42 | self.register_callbacks() 43 | 44 | def register_callbacks(self): 45 | on_query_completions_callbacks[self.input_panel.id()] = lambda prefix, locations: self.on_query_completions(prefix, locations) 46 | 47 | def on_query_completions(self, prefix, locations): 48 | if len(prefix) > 0: 49 | completions_list = get_sessions_list() 50 | #needed the "hit Tab" label due to https://github.com/SublimeTextIssues/Core/issues/1727 51 | completions_list = [["{0}\t hit Tab to insert".format(item), item] for item in completions_list if item.startswith(prefix)] 52 | return ( 53 | completions_list, 54 | sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS 55 | ) 56 | else: #if no prefix return None 57 | return 58 | 59 | def input_changed(self, session_name_prefix): 60 | """ 61 | on input changed open autocomplete menu with a delay 62 | """ 63 | if len(session_name_prefix) > 0 and \ 64 | self.input_panel and \ 65 | self.input_panel.command_history(0)[0] not in ['insert_completion', 'insert_best_completion']: #remove the looping 66 | if self.input_panel.is_auto_complete_visible(): 67 | self.input_panel.run_command('hide_auto_complete') 68 | delay = 500 69 | sublime.set_timeout(lambda: self.run_autocomplete(), delay) 70 | else: 71 | return 72 | 73 | def run_autocomplete(self): 74 | self.input_panel.run_command('auto_complete', {"disable_auto_insert": True}) 75 | 76 | def generate_name(self): 77 | nameformat = settings.get('session_name_format') 78 | return datetime.now().strftime(nameformat) 79 | 80 | def save_session(self, session_name): 81 | if not serialize.is_valid(session_name): 82 | error_message("invalid_name", session_name) 83 | self.run() 84 | return 85 | 86 | session = Session.save(session_name, sublime.windows()) 87 | try: 88 | serialize.dump(session_name, session) 89 | except OSError as e: 90 | error_message(e.errno) 91 | 92 | def is_enabled(self): 93 | windows = sublime.windows() 94 | for window in windows: 95 | if self.is_saveable(window): 96 | return True 97 | 98 | return False 99 | 100 | @staticmethod 101 | def is_saveable(window): 102 | return bool(window.views()) or bool(window.project_data()) 103 | 104 | 105 | class ListSessionCommand: 106 | def run(self): 107 | self.session_names = get_sessions_list() 108 | if not self.session_names: 109 | sublime.message_dialog(messages.message("no_sessions")) 110 | return 111 | 112 | sublime.active_window().show_quick_panel( 113 | self.session_names, 114 | self._handle_selection 115 | ) 116 | 117 | def _handle_selection(self, selected_index): 118 | if selected_index < 0: 119 | return 120 | 121 | self.handle_session(self.session_names[selected_index]) 122 | 123 | 124 | class LoadSession(ListSessionCommand, sublime_plugin.ApplicationCommand): 125 | def handle_session(self, session_name): 126 | try: 127 | session = serialize.load(session_name) 128 | except OSError as e: 129 | error_message(e.errno) 130 | else: 131 | session.load() 132 | 133 | 134 | class DeleteSession(ListSessionCommand, sublime_plugin.ApplicationCommand): 135 | def handle_session(self, session_name): 136 | try: 137 | serialize.delete(session_name) 138 | except OSError as e: 139 | error_message(e.errno) 140 | else: 141 | sublime.status_message(messages.message("deleted", session_name)) 142 | -------------------------------------------------------------------------------- /SessionManager.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // If session_path is null, the sessions will be saved in your sublime User folder in sessions 3 | // (User/sessions) 4 | "session_path": null, 5 | 6 | // The format which shall be used to generate the default session name; 7 | // Example result: session_15-02-07T15-09-32 8 | // take a look at the python docs for details: 9 | // https://docs.python.org/3.3/library/datetime.html#strftime-strptime-behavior 10 | "session_name_format": "session_%y-%m-%dT%H-%M-%S" 11 | } 12 | -------------------------------------------------------------------------------- /json/decoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sublime 3 | 4 | from ..modules import session 5 | 6 | 7 | def _objectify(s): 8 | if isinstance(s, str): 9 | return json.loads(s) 10 | 11 | return s 12 | 13 | 14 | class SessionDecoder(json.JSONDecoder): 15 | def decode(self, s): 16 | o = _objectify(s) 17 | try: 18 | name = o["name"] 19 | windows = [ 20 | WindowDecoder.decode(self, w) for w in o["windows"] 21 | ] 22 | except NameError: 23 | pass 24 | else: 25 | return session.Session(name, windows) 26 | 27 | return json.JSONDecoder.decode(self, o) 28 | 29 | 30 | class WindowDecoder(json.JSONDecoder): 31 | def decode(self, s): 32 | o = _objectify(s) 33 | try: 34 | project = o["project"] 35 | project_path = o["project_path"] 36 | views = [ 37 | ViewDecoder.decode(self, view) for view in o["views"] 38 | ] 39 | except NameError: 40 | pass 41 | else: 42 | return session.Window(project, project_path, views) 43 | 44 | return json.JSONDecoder.decode(self, o) 45 | 46 | 47 | class ViewDecoder(json.JSONDecoder): 48 | def decode(self, s): 49 | o = _objectify(s) 50 | try: 51 | file_path = o["file_path"] 52 | active = o["active"] 53 | sel_regions = [ 54 | RegionDecoder.decode(self, region) for region in o["sel_regions"] 55 | ] 56 | visible_region = RegionDecoder.decode(self, o["visible_region"]) 57 | except NameError: 58 | pass 59 | else: 60 | return session.View(file_path, active, sel_regions, visible_region) 61 | 62 | return json.JSONDecoder.decode(self, o) 63 | 64 | 65 | class RegionDecoder(json.JSONDecoder): 66 | def decode(self, s): 67 | o = _objectify(s) 68 | try: 69 | a = o[0] 70 | b = o[1] 71 | except IndexError: 72 | pass 73 | else: 74 | return sublime.Region(a, b) 75 | 76 | return json.JSONDecoder.decode(self, o) 77 | -------------------------------------------------------------------------------- /json/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sublime 3 | 4 | from ..modules import session 5 | 6 | 7 | class SessionEncoder(json.JSONEncoder): 8 | def default(self, obj): 9 | if isinstance(obj, session.Session): 10 | return { 11 | "name": obj.name, 12 | "windows": [ 13 | WindowEncoder.default(self, w) for w in obj.windows 14 | ] 15 | } 16 | 17 | return json.JSONEncoder.default(self, obj) 18 | 19 | 20 | class WindowEncoder(json.JSONEncoder): 21 | def default(self, obj): 22 | if isinstance(obj, session.Window): 23 | return { 24 | "project": obj.project, 25 | "project_path": obj.project_path, 26 | "views": [ 27 | ViewEncoder.default(self, v) for v in obj.views 28 | ] 29 | } 30 | 31 | return json.JSONEncoder.default(self, obj) 32 | 33 | 34 | class ViewEncoder(json.JSONEncoder): 35 | def default(self, obj): 36 | if isinstance(obj, session.View): 37 | return { 38 | "file_path": obj.file_path, 39 | "active": obj.active, 40 | "sel_regions": [ 41 | RegionEncoder.default(self, r) for r in obj.sel_regions 42 | ], 43 | "visible_region": RegionEncoder.default(self, obj.visible_region) 44 | } 45 | 46 | return json.JSONEncoder.default(self, obj) 47 | 48 | 49 | class RegionEncoder(json.JSONEncoder): 50 | def default(self, obj): 51 | if isinstance(obj, sublime.Region): 52 | return (obj.a, obj.b) 53 | 54 | return json.JSONEncoder.default(self, obj) 55 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/welcome" 3 | } 4 | -------------------------------------------------------------------------------- /messages/welcome: -------------------------------------------------------------------------------- 1 | Sublime-SessionManager 2 | ====================== 3 | 4 | Why? 5 | ---- 6 | 7 | Let's be honest: Sublime Text's session management is weird. 8 | It doesn't outright suck, but it's inflexible and hard to use if you don't have a project file. 9 | 10 | As long as you work in a single project everything is fine, but as soon as you want have multiple projects open at once, 11 | or just want to open a bunch of files and hack away, the sublime's session management screws you over. 12 | 13 | Sometimes you simply want to save your current state, do something different and come back to wherever you left. 14 | Preferably quick and uncomplicated. 15 | 16 | If you've ever been in such a situation, then this plugin is for you. 17 | 18 | How to use 19 | ---------- 20 | 21 | Using Session Manager you can "save", "load" and "delete" your current state. 22 | It handles all currently opened windows, folders and files, down to the currently visible region and all selections and cursors. 23 | 24 | When you load a session you can continue your work as if nothing ever happened. 25 | 26 | The commands, accessible through the command palette (ctrl+shift+p), are: 27 | 28 | Session Manager: Save Session 29 | Session Manager: Load Session 30 | Session Manager: Delete Session 31 | 32 | The default name for a session consists of the keyword session and the current timestamp (example: session_15-03-13T15-37-22). 33 | If you don't like the format, you can configure it with the "session_name_format" setting. 34 | 35 | Each of your sessions will be saved in a "Packages/User/sessions" folder. This can be changed via the "session_path" setting. 36 | 37 | The sessions are simple JSON files; this means you can edit and change them as you see fit. 38 | 39 | You can also bind the commands to the keyboard: 40 | 41 | Session Manager: Save Session is save_session, 42 | Session Manager: Load Session is load_session, and 43 | Session Manager: Delete Session is delete_session. 44 | 45 | 46 | Configuration 47 | ------------- 48 | 49 | Just take a look at the default configuration file to learn about the available options: 50 | 51 | { 52 | // If session_path is null, the sessions will be saved in your sublime User folder in sessions 53 | // (User/sessions) 54 | "session_path": null, 55 | 56 | // The format which shall be used to generate the default session name; 57 | // Example result: session_15-02-07T15-09-32 58 | // take a look at the python docs for details: 59 | // https://docs.python.org/3.3/library/datetime.html#strftime-strptime-behavior 60 | "session_name_format": "session_%y-%m-%dT%H-%M-%S" 61 | } 62 | 63 | 64 | Plans for the future 65 | -------------------- 66 | 67 | These are the features I'm planning to add in the future: 68 | 69 | - Save only current window 70 | - Close everything and open a new window after saving (configurable or another command?) 71 | - More information when loading sessions (so we can easier distinguish between them) 72 | 73 | If you think I should add some other features, then don't hesitate to open an issue. 74 | 75 | Happy session saving! 76 | -------------------------------------------------------------------------------- /modules/messages.py: -------------------------------------------------------------------------------- 1 | from os import strerror as _strerror 2 | from sys import platform as _platform 3 | 4 | _DEFAULT = "base" 5 | _DIALOG = "dialog" 6 | _GENERAL = "general" 7 | _ERROR = "error" 8 | 9 | _messages = { 10 | _DEFAULT: { 11 | _DIALOG: { 12 | "session_name": "Enter session name:" 13 | }, 14 | _GENERAL: { 15 | "no_sessions": "No sessions available.", 16 | "deleted": "Deleted session '{}'." 17 | }, 18 | _ERROR: { 19 | "invalid_name": "Invalid name \"{}\"!", 20 | "default": "Unknown error!" 21 | } 22 | }, 23 | "win32": { 24 | _ERROR: { 25 | "invalid_name": "Invalid name \"{}\"!\n\nForbidden characters: / ? < > \ : * | \"", 26 | } 27 | } 28 | } 29 | 30 | 31 | def get(group, key, *args): 32 | try: 33 | msg = _messages[_platform][group][key] 34 | except KeyError: 35 | msg = _messages[_DEFAULT][group][key] 36 | 37 | return str.format(msg, *args) 38 | 39 | 40 | def error(error_key, *args): 41 | if isinstance(error_key, int): 42 | msg = _errno(error_key, args) 43 | elif isinstance(error_key, str): 44 | msg = _error(error_key, args) 45 | 46 | return msg 47 | 48 | 49 | def _errno(error_code, args): 50 | try: 51 | msg = _strerror(error_code) 52 | except ValueError: 53 | msg = get(_ERROR, "default", *args) 54 | 55 | return str.format(msg + " (errno: {})", error_code) 56 | 57 | 58 | def _error(error_key, args): 59 | try: 60 | msg = get(_ERROR, error_key, *args) 61 | except KeyError: 62 | msg = get(_ERROR, "default", *args) 63 | 64 | return msg 65 | 66 | 67 | def dialog(key, *args): 68 | return get(_DIALOG, key, *args) 69 | 70 | 71 | def message(key, *args): 72 | return get(_GENERAL, key, *args) 73 | -------------------------------------------------------------------------------- /modules/serialize.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import glob 4 | import json 5 | import os 6 | 7 | from ..json import encoder 8 | from ..json import decoder 9 | 10 | from . import settings 11 | 12 | _DEFAULT_PATH = os.path.join('User', 'sessions') 13 | _DEFAULT_EXTENSION = '.sublime-session' 14 | 15 | 16 | def is_valid(name): 17 | session_path = _generate_path(name) 18 | try: 19 | open(session_path, 'w').close() 20 | os.unlink(session_path) 21 | except OSError: 22 | return False 23 | else: 24 | return True 25 | 26 | 27 | def dump(name, session): 28 | session_path = _generate_path(name) 29 | with open(session_path, 'w') as f: 30 | json.dump(session, f, cls=encoder.SessionEncoder) 31 | 32 | 33 | def load(name): 34 | session_path = _generate_path(name) 35 | with open(session_path, 'r') as f: 36 | return json.load(f, cls=decoder.SessionDecoder) 37 | 38 | 39 | def delete(name): 40 | session_path = _generate_path(name) 41 | os.remove(session_path) 42 | 43 | 44 | def available(): 45 | paths = _available_paths() 46 | files = [os.path.basename(p) for p in paths] 47 | # Remove the extension 48 | names = [f[:-len(_DEFAULT_EXTENSION)] for f in files] 49 | 50 | return names 51 | 52 | 53 | def _available_paths(): 54 | session_folder = _generate_folder() 55 | search_pattern = os.path.join( 56 | session_folder, 57 | ''.join(['*', _DEFAULT_EXTENSION]) 58 | ) 59 | 60 | return glob.glob(search_pattern) 61 | 62 | 63 | def _generate_path(name): 64 | return os.path.join(_generate_folder(), _generate_name(name)) 65 | 66 | 67 | def _generate_folder(): 68 | folder = settings.get('session_path') 69 | if folder: 70 | folder = os.path.normpath(folder) 71 | else: 72 | folder = os.path.join(sublime.packages_path(), _DEFAULT_PATH) 73 | 74 | # Ensure the folder exists 75 | try: 76 | os.makedirs(folder, exist_ok=True) 77 | except FileExistsError: 78 | # Issue 21082 (http://bugs.python.org/issue21082) 79 | # Before Python 3.4.1, if exist_ok was True and the directory existed, 80 | # makedirs() would still raise an error if mode did not match the mode of the existing directory 81 | pass 82 | 83 | return folder 84 | 85 | 86 | def _generate_name(name, extension=_DEFAULT_EXTENSION): 87 | return ''.join([name, extension]) 88 | -------------------------------------------------------------------------------- /modules/session.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from . import st_utils 4 | 5 | 6 | class Session: 7 | def __init__(self, name, window_sessions): 8 | self.name = name 9 | self.windows = window_sessions 10 | 11 | @classmethod 12 | def save(cls, name, st_windows): 13 | return cls( 14 | name, 15 | [Window.save(w) for w in st_windows] 16 | ) 17 | 18 | def load(self): 19 | for window in self.windows: 20 | window.load() 21 | 22 | 23 | class Window: 24 | def __init__(self, project, project_path, view_sessions): 25 | self.project = project 26 | self.project_path = project_path 27 | self.views = view_sessions 28 | 29 | @classmethod 30 | def save(cls, st_window): 31 | project = st_window.project_data() 32 | project_path = st_window.project_file_name() 33 | views = [View.save(v) for v in st_window.views()] 34 | 35 | return cls(project, project_path, views) 36 | 37 | def load(self): 38 | st_window = st_utils.open_window() 39 | 40 | self._load_project(st_window) 41 | self._load_views(st_window) 42 | 43 | def _load_project(self, st_window): 44 | hacked_project = st_utils.resolve_project_paths(self.project_path, self.project) 45 | st_window.set_project_data(hacked_project) 46 | 47 | def _load_views(self, st_window): 48 | # Workaround: Sublime focus bug on new views (issue #39) 49 | sublime.set_timeout(lambda: self._load_views_intern(st_window), 0) 50 | 51 | def _load_views_intern(self, st_window): 52 | for view in self.views: 53 | view.load(st_window) 54 | 55 | 56 | class View: 57 | def __init__(self, file_path, active, sel_regions, visible_region): 58 | self.file_path = file_path 59 | self.active = active 60 | self.sel_regions = sel_regions 61 | self.visible_region = visible_region 62 | 63 | @classmethod 64 | def save(cls, st_view): 65 | file_path = st_view.file_name() 66 | active = (st_view.id() == st_view.window().active_view().id()) 67 | sel_regions = [region for region in st_view.sel()] 68 | visible_region = st_view.visible_region() 69 | 70 | return cls(file_path, active, sel_regions, visible_region) 71 | 72 | def load(self, st_window): 73 | view = st_window.open_file(self.file_path) 74 | sublime.set_timeout(lambda: self._init_view(view), 50) 75 | 76 | def _init_view(self, view): 77 | if view.is_loading(): 78 | sublime.set_timeout(lambda: self._init_view(view), 50) 79 | return 80 | 81 | selection = view.sel() 82 | selection.clear() 83 | selection.add_all(self.sel_regions) 84 | 85 | view.show_at_center(self.visible_region) 86 | 87 | if self.active: 88 | view.window().focus_view(view) 89 | -------------------------------------------------------------------------------- /modules/settings.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | _settings_file = "SessionManager.sublime-settings" 4 | _subl_settings = {} 5 | 6 | 7 | def load(): 8 | global _subl_settings 9 | 10 | _subl_settings = sublime.load_settings(_settings_file) 11 | 12 | 13 | def get(key): 14 | return _subl_settings.get(key) 15 | -------------------------------------------------------------------------------- /modules/st_utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from os import path 4 | 5 | 6 | def open_window(): 7 | sublime.run_command("new_window") 8 | return sublime.active_window() 9 | 10 | 11 | def resolve_project_paths(project_file_path, project_data): 12 | if not project_data: 13 | return None 14 | if not project_file_path: 15 | return project_data 16 | 17 | resolved = dict(project_data) 18 | basefolder = path.dirname(project_file_path) 19 | for folder in resolved['folders']: 20 | folder_path = folder['path'] 21 | if path.isabs(folder_path): 22 | continue 23 | 24 | new_path = path.join(basefolder, folder_path) 25 | new_path = path.normpath(new_path) 26 | folder['path'] = new_path 27 | 28 | return resolved 29 | -------------------------------------------------------------------------------- /session_manager.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": "." 7 | } 8 | ] 9 | } 10 | --------------------------------------------------------------------------------