├── .gitignore ├── SBTError.hidden-tmLanguage ├── LICENSE ├── errorreporter.py ├── SublimeSBT.sublime-settings ├── util.py ├── SBTOutput.hidden-tmLanguage ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── sbterror.py ├── SBTOutput.hidden-tmTheme ├── errorview.py ├── sbtsettings.py ├── errormarker.py ├── Default (OSX).sublime-keymap ├── project.py ├── errorreport.py ├── Main.sublime-menu ├── highlighter.py ├── sbtview.py ├── outputmon.py ├── README.md ├── sbtrunner.py └── sublimesbt.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | package-metadata.json 3 | *tmLanguage.json 4 | *tmTheme.json 5 | *.cache 6 | -------------------------------------------------------------------------------- /SBTError.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | SBT Error 7 | patterns 8 | 9 | 10 | match 11 | -- Error -- 12 | name 13 | entity.label.error.sbt-error 14 | 15 | 16 | match 17 | -- Test Failure -- 18 | name 19 | entity.label.error.sbt-error 20 | 21 | 22 | match 23 | -- Warning -- 24 | name 25 | entity.label.warning.sbt-error 26 | 27 | 28 | scopeName 29 | error.sbt 30 | uuid 31 | dc4ce12f-5399-4d33-947e-fc339c5d567f 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jason Arhart 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /errorreporter.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .errormarker import ErrorMarker 3 | from .util import delayed 4 | except(ValueError): 5 | from errormarker import ErrorMarker 6 | from util import delayed 7 | 8 | 9 | class ErrorReporter(object): 10 | 11 | def __init__(self, window, error_report, settings): 12 | self._marker = ErrorMarker(window, error_report, settings) 13 | self._error_report = error_report 14 | 15 | def error(self, error): 16 | self._error_report.add_error(error) 17 | self._marker.mark_error(error) 18 | self._marker.update_status() 19 | 20 | def finish(self): 21 | self._error_report.cycle() 22 | self._marker.mark_errors() 23 | 24 | def clear(self): 25 | self._error_report.clear() 26 | self._marker.clear() 27 | 28 | def show_errors(self): 29 | self._marker.mark_errors() 30 | 31 | def show_errors_in(self, filename): 32 | self._marker.mark_errors_in(filename) 33 | 34 | def hide_errors_in(self, filename): 35 | self._error_report.clear_file(filename) 36 | self._marker.hide_errors_in(filename) 37 | 38 | def update_status(self): 39 | self._marker.update_status() 40 | 41 | def update_status_now(self): 42 | self._marker.update_status_now() 43 | -------------------------------------------------------------------------------- /SublimeSBT.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // An array representing the command line to use to start sbt. Depending on 3 | // your setup you may need to put the full path to the file here. 4 | "sbt_command": ["sbt"], 5 | 6 | // An array representing the command line to use to start sbt for a Play 7 | // Framework project. Depending on your setup you may need to put the full 8 | // path to the file here. 9 | "play_command": ["play"], 10 | 11 | // A string representing the sbt command to use to run tests. 12 | "test_command": "test", 13 | 14 | // A string representing the sbt command to use to run the project. 15 | "run_command": "run", 16 | 17 | // The output encoding to use when running sbt 18 | "encoding": "UTF-8", 19 | 20 | // How to mark errors in the source code. 21 | "error_marking": { 22 | 23 | // The mark style to us. Valid values are "dot", "outline", or "both". 24 | "style": "both", 25 | 26 | // The scope used to color the outline. 27 | "scope": "invalid.illegal" 28 | }, 29 | 30 | "failure_marking": { 31 | "style": "both", 32 | "scope": "invalid.illegal" 33 | }, 34 | 35 | "warning_marking": { 36 | "style": "both", 37 | "scope": "source.scala" 38 | }, 39 | 40 | // The color scheme to use for the output panel. 41 | "color_scheme": "Packages/SublimeSBT/SBTOutput.hidden-tmTheme", 42 | 43 | // The maximum number of unique entries to keep in the command history 44 | "history_length": 20 45 | } 46 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import functools 4 | import itertools 5 | import threading 6 | 7 | 8 | def maybe(value): 9 | if value is not None: 10 | yield value 11 | 12 | 13 | def group_by(xs, kf): 14 | grouped = {} 15 | for k, i in itertools.groupby(xs, kf): 16 | grouped.setdefault(k, []).extend(i) 17 | return grouped 18 | 19 | 20 | class delayed(object): 21 | 22 | def __init__(self, timeout): 23 | self.timeout = timeout 24 | 25 | def __call__(self, f): 26 | 27 | def call_with_timeout(*args, **kwargs): 28 | sublime.set_timeout(functools.partial(f, *args, **kwargs), 29 | self.timeout) 30 | 31 | return call_with_timeout 32 | 33 | 34 | class SynchronizedCache(object): 35 | 36 | def __init__(self): 37 | self.__items = {} 38 | self.__lock = threading.RLock() 39 | 40 | def __call__(self, key, f): 41 | with self.__lock: 42 | if key not in self.__items: 43 | self.__items[key] = f() 44 | return self.__items[key] 45 | 46 | 47 | class MetaOnePerWindow(type): 48 | 49 | def __init__(cls, name, bases, dct): 50 | super(MetaOnePerWindow, cls).__init__(name, bases, dct) 51 | cls.instance_cache = SynchronizedCache() 52 | 53 | def __call__(cls, window): 54 | return cls.instance_cache(window.id(), lambda: type.__call__(cls, window)) 55 | 56 | 57 | OnePerWindow = MetaOnePerWindow('OnePerWindow', (object,), {}) 58 | -------------------------------------------------------------------------------- /SBTOutput.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | SBT Output 7 | patterns 8 | 9 | 10 | captures 11 | 12 | 1 13 | 14 | name 15 | entity.label.success.sbt-output 16 | 17 | 18 | match 19 | ^\[(success)\] 20 | name 21 | meta.label.sbt-output 22 | 23 | 24 | captures 25 | 26 | 1 27 | 28 | name 29 | entity.label.error.sbt-output 30 | 31 | 32 | match 33 | ^\[(error)\] 34 | name 35 | meta.label.sbt-output 36 | 37 | 38 | captures 39 | 40 | 1 41 | 42 | name 43 | entity.label.warning.sbt-output 44 | 45 | 46 | match 47 | ^\[(warn)\] 48 | name 49 | meta.label.sbt-output 50 | 51 | 52 | captures 53 | 54 | 1 55 | 56 | name 57 | entity.label.other.sbt-output 58 | 59 | 60 | match 61 | ^\[([a-z]+)\] 62 | name 63 | meta.label.sbt-output 64 | 65 | 66 | scopeName 67 | output.sbt 68 | uuid 69 | aeb00e40-9cfb-4b4d-96b3-767d9381eaff 70 | 71 | 72 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+o"], "command": "show_sbt" }, 3 | 4 | { "keys": ["ctrl+alt+l"], "command": "list_sbt_errors" }, 5 | { "keys": ["ctrl+alt+n"], "command": "next_sbt_error" }, 6 | { "keys": ["ctrl+alt+o"], "command": "show_sbt_error_output" }, 7 | 8 | { "keys": ["ctrl+alt+h"], "command": "sbt_show_history" }, 9 | { "keys": ["ctrl+alt+shift+h"], "command": "sbt_show_history", "args": {"editable": true} }, 10 | 11 | { "keys": ["enter"], "command": "sbt_submit", "context": [{ "key": "in_sbt_view" }] }, 12 | { "keys": ["ctrl+d"], "command": "sbt_eot", "context": [{ "key": "in_sbt_view" }] }, 13 | 14 | { "keys": ["backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 15 | { "keys": ["shift+backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 16 | 17 | { "keys": ["ctrl+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 18 | { "keys": ["ctrl+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 19 | 20 | // Disable keys that can modify the output 21 | { "keys": ["ctrl+shift+backspace"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 22 | { "keys": ["ctrl+x"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 23 | { "keys": ["ctrl+shift+k"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 24 | { "keys": ["ctrl+shift+up"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 25 | { "keys": ["ctrl+shift+down"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 26 | { "keys": ["ctrl+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 27 | { "keys": ["ctrl+shift+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 28 | { "keys": ["ctrl+j"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 29 | { "keys": ["ctrl+shift+d"], "command": "noop", "context": [{ "key": "in_sbt_view" }] } 30 | ] 31 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+o"], "command": "show_sbt" }, 3 | 4 | { "keys": ["ctrl+alt+l"], "command": "list_sbt_errors" }, 5 | { "keys": ["ctrl+alt+n"], "command": "next_sbt_error" }, 6 | { "keys": ["ctrl+alt+o"], "command": "show_sbt_error_output" }, 7 | 8 | { "keys": ["ctrl+alt+h"], "command": "sbt_show_history" }, 9 | { "keys": ["ctrl+alt+shift+h"], "command": "sbt_show_history", "args": {"editable": true} }, 10 | 11 | { "keys": ["enter"], "command": "sbt_submit", "context": [{ "key": "in_sbt_view" }] }, 12 | { "keys": ["ctrl+d"], "command": "sbt_eot", "context": [{ "key": "in_sbt_view" }] }, 13 | 14 | { "keys": ["backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 15 | { "keys": ["shift+backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 16 | 17 | { "keys": ["ctrl+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 18 | { "keys": ["ctrl+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 19 | 20 | // Disable keys that can modify the output 21 | { "keys": ["ctrl+shift+backspace"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 22 | { "keys": ["ctrl+x"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 23 | { "keys": ["ctrl+shift+k"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 24 | { "keys": ["ctrl+shift+up"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 25 | { "keys": ["ctrl+shift+down"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 26 | { "keys": ["ctrl+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 27 | { "keys": ["ctrl+shift+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 28 | { "keys": ["ctrl+j"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 29 | { "keys": ["ctrl+shift+d"], "command": "noop", "context": [{ "key": "in_sbt_view" }] } 30 | ] 31 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "SBT: Start SBT", "command": "start_sbt" }, 3 | { "caption": "SBT: Stop SBT", "command": "stop_sbt" }, 4 | { "caption": "SBT: Kill SBT", "command": "kill_sbt" }, 5 | { "caption": "SBT: Show SBT Output", "command": "show_sbt" }, 6 | { "caption": "SBT: Compile", "command": "sbt", "args": {"command": "compile"} }, 7 | { "caption": "SBT: Test:Compile", "command": "sbt", "args": {"command": "test:compile"} }, 8 | { "caption": "SBT: Test", "command": "sbt_test" }, 9 | { "caption": "SBT: Test-Only", "command": "sbt_test_only" }, 10 | { "caption": "SBT: Test-Quick", "command": "sbt_test_quick" }, 11 | { "caption": "SBT: Run", "command": "sbt_run" }, 12 | { "caption": "SBT: Package", "command": "sbt", "args": {"command": "package"} }, 13 | { "caption": "SBT: Start Console", "command": "sbt", "args": {"command": "console"} }, 14 | { "caption": "SBT: Start Continuous Compiling", "command": "sbt", "args": {"command": "~ compile"} }, 15 | { "caption": "SBT: Start Continuous Test:Compiling", "command": "sbt", "args": {"command": "~ test:compile"} }, 16 | { "caption": "SBT: Start Continuous Testing", "command": "sbt_continuous_test" }, 17 | { "caption": "SBT: Start Continuous Test-Only", "command": "sbt_continuous_test_only" }, 18 | { "caption": "SBT: Start Continuous Test-Quick", "command": "sbt_continuous_test_quick" }, 19 | { "caption": "SBT: Reload", "command": "sbt_reload" }, 20 | { "caption": "SBT: Show Error List", "command": "list_sbt_errors" }, 21 | { "caption": "SBT: Show Next Error", "command": "next_sbt_error" }, 22 | { "caption": "SBT: Clear Errors", "command": "clear_sbt_errors" }, 23 | { "caption": "SBT: Show Error Output", "command": "show_sbt_error_output" }, 24 | { "caption": "SBT: Show History", "command": "sbt_show_history" }, 25 | { "caption": "SBT: Show History and Edit", "command": "sbt_show_history", "args": {"editable": true} }, 26 | { "caption": "SBT: Clear History", "command": "sbt_clear_history" } 27 | ] 28 | -------------------------------------------------------------------------------- /sbterror.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .util import delayed 3 | except(ValueError): 4 | from util import delayed 5 | 6 | from threading import Event 7 | 8 | import re 9 | 10 | class SbtError(object): 11 | 12 | def __init__(self, project, filename, line, message, error_type, extra_lines): 13 | self.line = int(line) 14 | if len(extra_lines) > 0 and re.match(r' *^', extra_lines[-1]): 15 | self.column_spec = ':%i' % len(extra_lines[-1]) 16 | else: 17 | self.column_spec = '' 18 | self.message = message 19 | self.error_type = error_type 20 | self.__finished = Event() 21 | self.__finish(project, filename, extra_lines) 22 | 23 | @property 24 | def filename(self): 25 | self.__finished.wait() 26 | return self.__filename 27 | 28 | @property 29 | def relative_path(self): 30 | self.__finished.wait() 31 | return self.__relative_path 32 | 33 | @property 34 | def text(self): 35 | self.__finished.wait() 36 | return self.__text 37 | 38 | def list_item(self): 39 | return [self.message, '%s:%i%s' % (self.relative_path, self.line, self.column_spec)] 40 | 41 | def encoded_position(self): 42 | return '%s:%i%s' % (self.filename, self.line, self.column_spec) 43 | 44 | @delayed(0) 45 | def __finish(self, project, filename, extra_lines): 46 | try: 47 | self.__filename = project.expand_filename(filename) 48 | if self.__filename: 49 | self.__relative_path = project.relative_path(self.__filename) 50 | if self.error_type == 'failure': 51 | self.__text = '%s (%s:%i)' % (self.message, filename, self.line) 52 | else: 53 | extra_lines.insert(0, '%s:%i: %s' % (self.__relative_path, self.line, self.message)) 54 | self.__text = '\n'.join(extra_lines) 55 | finally: 56 | self.__finished.set() 57 | -------------------------------------------------------------------------------- /SBTOutput.hidden-tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | SBT Output 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #000000 14 | caret 15 | #999999 16 | foreground 17 | #DDDDDD 18 | invisibles 19 | #333333 20 | lineHighlight 21 | #222222 22 | selection 23 | #444444 24 | 25 | 26 | 27 | name 28 | Success Label 29 | scope 30 | entity.label.success.sbt-output 31 | settings 32 | 33 | foreground 34 | #00EE00 35 | 36 | 37 | 38 | name 39 | Error Label 40 | scope 41 | entity.label.error.sbt-output, entity.label.error.sbt-error 42 | settings 43 | 44 | foreground 45 | #FF0000 46 | 47 | 48 | 49 | name 50 | Warning Label 51 | scope 52 | entity.label.warning.sbt-output, entity.label.warning.sbt-error 53 | settings 54 | 55 | foreground 56 | #DDDD00 57 | 58 | 59 | 60 | name 61 | Output Label 62 | scope 63 | meta.label.sbt-output 64 | settings 65 | 66 | foreground 67 | #AAAAAA 68 | 69 | 70 | 71 | uuid 72 | 7b2b9f2b-a2d6-4270-8639-8c898d0fe0ef 73 | 74 | 75 | -------------------------------------------------------------------------------- /errorview.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | try: 5 | from .sbtsettings import SBTSettings 6 | from .util import OnePerWindow 7 | except(ValueError): 8 | from sbtsettings import SBTSettings 9 | from util import OnePerWindow 10 | 11 | 12 | class ErrorView(OnePerWindow): 13 | 14 | error_type_display = { 15 | 'error': 'Error', 16 | 'warning': 'Warning', 17 | 'failure': 'Test Failure' 18 | } 19 | 20 | def __init__(self, window): 21 | self.window = window 22 | self.settings = SBTSettings(window) 23 | self.panel = self.window.get_output_panel('sbt_error') 24 | self.panel.set_read_only(True) 25 | self.panel.settings().set('line_numbers', False) 26 | self.panel.settings().set('gutter', False) 27 | self.panel.settings().set('scroll_past_end', False) 28 | self.panel.set_syntax_file("Packages/SublimeSBT/SBTError.hidden-tmLanguage") 29 | self._update_panel_colors() 30 | self.settings.add_on_change(self._update_panel_colors) 31 | 32 | def show(self): 33 | self._update_panel_colors() 34 | self.window.run_command('show_panel', {'panel': 'output.sbt_error'}) 35 | 36 | def hide(self): 37 | self.window.run_command('hide_panel', {'panel': 'output.sbt_error'}) 38 | 39 | def show_error(self, error): 40 | self.show() 41 | self.panel.run_command('sbt_show_error_text', 42 | {'text': self._error_text(error)}) 43 | self.panel.sel().clear() 44 | self.panel.show(0) 45 | 46 | def clear(self): 47 | self.panel.run_command('sbt_show_error_text', {'text': ''}) 48 | self.panel.sel().clear() 49 | 50 | def _error_text(self, error): 51 | banner = ' -- %s --' % type(self).error_type_display[error.error_type] 52 | return '%s\n%s' % (banner, error.text) 53 | 54 | def _update_panel_colors(self): 55 | self.panel.settings().set('color_scheme', self.settings.get('color_scheme')) 56 | 57 | 58 | class SbtShowErrorTextCommand(sublime_plugin.TextCommand): 59 | 60 | def run(self, edit, text): 61 | self.view.set_read_only(False) 62 | self.view.replace(edit, sublime.Region(0, self.view.size()), text) 63 | self.view.set_read_only(True) 64 | -------------------------------------------------------------------------------- /sbtsettings.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | try: 4 | from .util import maybe 5 | except(ValueError): 6 | from util import maybe 7 | 8 | 9 | class SBTSettings(object): 10 | 11 | def __init__(self, window): 12 | self.window = window 13 | self._plugin_settings = sublime.load_settings('SublimeSBT.sublime-settings') 14 | self._migrate_user_config() 15 | 16 | def sbt_command(self): 17 | return self.get('sbt_command') 18 | 19 | def play_command(self): 20 | return self._view_settings().get('sbt_command', self._plugin_settings.get('play_command')) 21 | 22 | def test_command(self): 23 | return self.get('test_command') 24 | 25 | def run_command(self): 26 | return self.get('run_command') 27 | 28 | def mark_style(self, error_type='error'): 29 | self.mark_settings(error_type).get('style') 30 | 31 | def error_scope(self, error_type='error'): 32 | self.mark_settings(error_type).get('scope') 33 | 34 | def color_scheme(self): 35 | return self.get('color_scheme') 36 | 37 | def mark_settings(self, error_type='error'): 38 | for settings in maybe(self.get('%s_marking' % error_type)): 39 | return settings 40 | return self.global_mark_settings() 41 | 42 | def global_mark_settings(self): 43 | return { 44 | 'style': self.get('mark_style'), 45 | 'scope': self.get('error_scope') 46 | } 47 | 48 | def get(self, name): 49 | return self._view_settings().get(name, self._plugin_settings.get(name)) 50 | 51 | def add_on_change(self, on_change): 52 | self._plugin_settings.add_on_change('SublimeSBT', on_change) 53 | 54 | def _view_settings(self): 55 | for view in maybe(self.window.active_view()): 56 | return view.settings().get('SublimeSBT', {}) 57 | return {} 58 | 59 | def _migrate_user_config(self): 60 | style = self._plugin_settings.get('mark_style', None) 61 | scope = self._plugin_settings.get('error_scope', None) 62 | if style is not None or scope is not None: 63 | for key in ('%s_marking' % t for t in ('error', 'failure', 'warning')): 64 | mark_settings = self._plugin_settings.get(key, {}) 65 | if style is not None: 66 | mark_settings['style'] = style 67 | if scope is not None: 68 | mark_settings['scope'] = scope 69 | self._plugin_settings.set(key, mark_settings) 70 | self._plugin_settings.erase('mark_style') 71 | self._plugin_settings.erase('error_scope') 72 | sublime.save_settings('SublimeSBT.sublime-settings') 73 | -------------------------------------------------------------------------------- /errormarker.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .highlighter import CodeHighlighter 3 | from .util import delayed, maybe 4 | except(ValueError): 5 | from highlighter import CodeHighlighter 6 | from util import delayed, maybe 7 | 8 | 9 | class ErrorMarker(object): 10 | 11 | def __init__(self, window, error_report, settings): 12 | self._window = window 13 | self._error_report = error_report 14 | self.__settings = settings 15 | self.__highlighter = None 16 | settings.add_on_change(self.mark_errors) 17 | 18 | @delayed(0) 19 | def mark_errors(self): 20 | for view in self._window.views(): 21 | errors = self._error_report.sorted_errors_in(view.file_name()) 22 | self._mark_errors_in_view(view, errors) 23 | 24 | @delayed(0) 25 | def mark_errors_in(self, filename): 26 | errors = self._error_report.sorted_errors_in(filename) 27 | for view in self._file_views(filename): 28 | self._mark_errors_in_view(view, errors) 29 | 30 | @delayed(0) 31 | def hide_errors_in(self, filename): 32 | for view in self._file_views(filename): 33 | self._highlighter.clear(view) 34 | 35 | @delayed(0) 36 | def mark_error(self, error): 37 | for view in self._file_views(error.filename): 38 | self._highlighter.highlight(view, [error]) 39 | 40 | @delayed(0) 41 | def clear(self): 42 | for view in self._window.views(): 43 | self._highlighter.clear(view) 44 | 45 | @delayed(0) 46 | def update_status(self): 47 | self.update_status_now() 48 | 49 | def update_status_now(self): 50 | for view in maybe(self._window.active_view()): 51 | self._highlighter.set_status_message(view, self._status_message(view)) 52 | 53 | def _mark_errors_in_view(self, view, errors): 54 | if errors and not view.is_dirty(): 55 | self._highlighter.highlight(view, errors, replace=True) 56 | else: 57 | self._highlighter.clear(view) 58 | 59 | def _status_message(self, view): 60 | for errors in maybe(self._line_errors(view)): 61 | return '(%s)' % ')('.join([e.message for e in errors]) 62 | 63 | def _file_views(self, filename): 64 | for view in self._window.views(): 65 | if filename == view.file_name(): 66 | yield view 67 | 68 | def _line_errors(self, view): 69 | row, _ = view.rowcol(view.sel()[0].begin()) 70 | return self._error_report.errors_at(view.file_name(), row + 1) 71 | 72 | def _current_error_in_view(self, view): 73 | return self._error_report.current_error_in(view.file_name()) 74 | 75 | @property 76 | def _highlighter(self): 77 | if self.__highlighter is None: 78 | self.__highlighter = CodeHighlighter(self.__settings, self._current_error_in_view) 79 | return self.__highlighter 80 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+o"], "command": "show_sbt" }, 3 | 4 | { "keys": ["ctrl+alt+l"], "command": "list_sbt_errors" }, 5 | { "keys": ["ctrl+alt+n"], "command": "next_sbt_error" }, 6 | { "keys": ["ctrl+alt+o"], "command": "show_sbt_error_output" }, 7 | 8 | { "keys": ["ctrl+alt+h"], "command": "sbt_show_history" }, 9 | { "keys": ["ctrl+alt+shift+h"], "command": "sbt_show_history", "args": {"editable": true} }, 10 | 11 | { "keys": ["enter"], "command": "sbt_submit", "context": [{ "key": "in_sbt_view" }] }, 12 | { "keys": ["ctrl+d"], "command": "sbt_eot", "context": [{ "key": "in_sbt_view" }] }, 13 | 14 | { "keys": ["backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 15 | { "keys": ["shift+backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 16 | { "keys": ["ctrl+shift+backspace"], "command": "sbt_delete_left", "context": [{ "key": "in_sbt_view" }] }, 17 | 18 | { "keys": ["ctrl+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 19 | { "keys": ["alt+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 20 | { "keys": ["ctrl+alt+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 21 | { "keys": ["alt+shift+backspace"], "command": "sbt_delete_word_left", "context": [{ "key": "in_sbt_view" }] }, 22 | 23 | { "keys": ["ctrl+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 24 | { "keys": ["alt+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 25 | { "keys": ["ctrl+alt+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 26 | { "keys": ["alt+shift+delete"], "command": "sbt_delete_word_right", "context": [{ "key": "in_sbt_view" }] }, 27 | 28 | { "keys": ["super+backspace"], "command": "sbt_delete_bol", "context": [{ "key": "in_sbt_view" }] }, 29 | { "keys": ["super+shift+backspace"], "command": "sbt_delete_bol", "context": [{ "key": "in_sbt_view" }] }, 30 | 31 | // Disable keys that can modify the output 32 | { "keys": ["ctrl+alt+shift+backspace"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 33 | { "keys": ["ctrl+x"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 34 | { "keys": ["ctrl+shift+k"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 35 | { "keys": ["super+k", "super+backspace"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 36 | { "keys": ["ctrl+super+up"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 37 | { "keys": ["ctrl+super+down"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 38 | { "keys": ["super+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 39 | { "keys": ["super+alt+forward_slash"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 40 | { "keys": ["super+j"], "command": "noop", "context": [{ "key": "in_sbt_view" }] }, 41 | { "keys": ["super+shift+d"], "command": "noop", "context": [{ "key": "in_sbt_view" }] } 42 | ] 43 | -------------------------------------------------------------------------------- /project.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | try: 4 | from .sbtsettings import SBTSettings 5 | from .errorreport import ErrorReport 6 | from .errorreporter import ErrorReporter 7 | from .util import maybe, OnePerWindow 8 | except(ValueError): 9 | from sbtsettings import SBTSettings 10 | from errorreport import ErrorReport 11 | from errorreporter import ErrorReporter 12 | from util import maybe, OnePerWindow 13 | 14 | import os 15 | import re 16 | from glob import glob 17 | 18 | 19 | class Project(OnePerWindow): 20 | 21 | def __init__(self, window): 22 | self.window = window 23 | self.settings = SBTSettings(window) 24 | self.error_report = ErrorReport() 25 | self.error_reporter = ErrorReporter(window, 26 | self.error_report, 27 | self.settings) 28 | 29 | def project_root(self): 30 | for folder in self.window.folders(): 31 | if self._is_sbt_folder(folder): 32 | return folder 33 | 34 | def is_sbt_project(self): 35 | return self.project_root() is not None 36 | 37 | def is_play_project(self): 38 | for root in maybe(self.project_root()): 39 | if self._play_build_files(root): 40 | return True 41 | 42 | def sbt_command(self): 43 | if self.is_play_project(): 44 | return self.settings.play_command() 45 | else: 46 | return self.settings.sbt_command() 47 | 48 | def setting(self, name): 49 | return self.settings.get(name) 50 | 51 | def expand_filename(self, filename): 52 | if len(os.path.dirname(filename)) > 0: 53 | return filename 54 | else: 55 | return self._find_in_project(filename) 56 | 57 | def relative_path(self, filename): 58 | return os.path.relpath(filename, self.project_root()) 59 | 60 | def open_project_file(self, filename, line): 61 | full_path = os.path.join(self.project_root(), filename) 62 | self.window.open_file('%s:%i' % (full_path, line), 63 | sublime.ENCODED_POSITION) 64 | 65 | def _is_sbt_folder(self, folder): 66 | if self._sbt_build_files(folder) or self._scala_build_files(folder): 67 | return True 68 | 69 | def _sbt_build_files(self, folder): 70 | return glob(os.path.join(folder, '*.sbt')) 71 | 72 | def _scala_build_files(self, folder): 73 | return glob(os.path.join(folder, 'project', '*.scala')) 74 | 75 | def _play_build_files(self, folder): 76 | return list(filter(self._is_play_build, self._scala_build_files(folder))) 77 | 78 | def _is_play_build(self, build_path): 79 | try: 80 | with open(build_path, 'r') as build_file: 81 | for line in build_file.readlines(): 82 | if re.search(r'\b(?:play\.|Play)Project\b', line): 83 | return True 84 | except: 85 | return False 86 | 87 | def _find_in_project(self, filename): 88 | for path, _, files in os.walk(self.project_root()): 89 | if filename in files: 90 | return os.path.join(path, filename) 91 | -------------------------------------------------------------------------------- /errorreport.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .sbterror import SbtError 3 | from .util import maybe 4 | except(ValueError): 5 | from sbterror import SbtError 6 | from util import maybe 7 | 8 | 9 | class ErrorReport(object): 10 | 11 | def __init__(self): 12 | self._errors = {} 13 | self._old_errors = {} 14 | self._new_errors = {} 15 | self._set_current(None) 16 | 17 | def clear(self): 18 | self._errors.clear() 19 | self._old_errors.clear() 20 | self._new_errors.clear() 21 | self._set_current(None) 22 | 23 | def add_error(self, error): 24 | if error.filename: 25 | if error.filename not in self._new_errors: 26 | self._new_errors[error.filename] = {} 27 | file_errors = self._new_errors[error.filename] 28 | if error.line not in file_errors: 29 | file_errors[error.line] = [] 30 | file_errors[error.line].append(error) 31 | self._merge_errors() 32 | 33 | def cycle(self): 34 | self._old_errors = self._new_errors 35 | self._new_errors = {} 36 | self._merge_errors() 37 | 38 | def all_errors(self): 39 | for filename in sorted(self._errors.keys()): 40 | for error in self.sorted_errors_in(filename): 41 | yield error 42 | 43 | def focus_error(self, error): 44 | for i, e in enumerate(self.all_errors()): 45 | if e == error: 46 | self._set_current(i) 47 | 48 | def next_error(self): 49 | sorted_errors = list(self.all_errors()) 50 | if sorted_errors: 51 | if self._index is None: 52 | self._set_current(0) 53 | else: 54 | self._set_current((self._index + 1) % len(sorted_errors)) 55 | else: 56 | self._set_current(None) 57 | return self.current_error 58 | 59 | def sorted_errors_in(self, filename): 60 | 61 | def sort_errors(errors): 62 | for line in sorted(errors.keys()): 63 | for error in sorted(errors[line], key=lambda e: e.error_type): 64 | yield error 65 | 66 | for errors in maybe(self.errors_in(filename)): 67 | return list(sort_errors(errors)) 68 | 69 | def errors_at(self, filename, line): 70 | for errors in maybe(self.errors_in(filename)): 71 | return errors.get(line) 72 | 73 | def errors_in(self, filename): 74 | return self._errors.get(filename) 75 | 76 | def current_error_in(self, filename): 77 | for error in maybe(self.current_error): 78 | if error.filename == filename: 79 | return error 80 | 81 | def clear_file(self, filename): 82 | for errors in [self._old_errors, self._new_errors, self._errors]: 83 | if filename in errors: 84 | del errors[filename] 85 | if self.current_error_in(filename): 86 | self._set_current(None) 87 | 88 | def has_errors(self): 89 | return len(self._errors) > 0 90 | 91 | def _merge_errors(self): 92 | self._errors = dict(list(self._old_errors.items()) + list(self._new_errors.items())) 93 | self._set_current(None) 94 | 95 | def _set_current(self, i): 96 | sorted_errors = list(self.all_errors()) 97 | if i is not None and i < len(sorted_errors): 98 | self._index = i 99 | self.current_error = sorted_errors[i] 100 | else: 101 | self._index = None 102 | self.current_error = None 103 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "SublimeSBT", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "args": {"file": "${packages}/SublimeSBT/README.md"}, 21 | "caption": "README" 22 | }, 23 | { "caption": "-" }, 24 | { 25 | "command": "open_file", "args": 26 | { 27 | "file": "${packages}/SublimeSBT/SublimeSBT.sublime-settings" 28 | }, 29 | "caption": "Settings – Default" 30 | }, 31 | { 32 | "command": "open_file", "args": 33 | { 34 | "file": "${packages}/User/SublimeSBT.sublime-settings" 35 | }, 36 | "caption": "Settings – User" 37 | }, 38 | { "caption": "-" }, 39 | { 40 | "command": "open_file", 41 | "args": { 42 | "file": "${packages}/SublimeSBT/Default (OSX).sublime-keymap", 43 | "platform": "OSX" 44 | }, 45 | "caption": "Key Bindings – Default" 46 | }, 47 | { 48 | "command": "open_file", 49 | "args": { 50 | "file": "${packages}/SublimeSBT/Default (Linux).sublime-keymap", 51 | "platform": "Linux" 52 | }, 53 | "caption": "Key Bindings – Default" 54 | }, 55 | { 56 | "command": "open_file", 57 | "args": { 58 | "file": "${packages}/SublimeSBT/Default (Windows).sublime-keymap", 59 | "platform": "Windows" 60 | }, 61 | "caption": "Key Bindings – Default" 62 | }, 63 | { 64 | "command": "open_file", 65 | "args": { 66 | "file": "${packages}/User/Default (OSX).sublime-keymap", 67 | "platform": "OSX" 68 | }, 69 | "caption": "Key Bindings – User" 70 | }, 71 | { 72 | "command": "open_file", 73 | "args": { 74 | "file": "${packages}/User/Default (Linux).sublime-keymap", 75 | "platform": "Linux" 76 | }, 77 | "caption": "Key Bindings – User" 78 | }, 79 | { 80 | "command": "open_file", 81 | "args": { 82 | "file": "${packages}/User/Default (Windows).sublime-keymap", 83 | "platform": "Windows" 84 | }, 85 | "caption": "Key Bindings – User" 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /highlighter.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | try: 4 | from .util import group_by, maybe 5 | except(ValueError): 6 | from util import group_by, maybe 7 | 8 | 9 | class CodeHighlighter(object): 10 | 11 | error_types = ['error', 'failure', 'warning'] 12 | 13 | def __init__(self, settings, current_error_in_view): 14 | self.settings = settings 15 | self._current_error_in_view = current_error_in_view 16 | self.bookmark_key = 'sublimesbt_bookmark' 17 | self.status_key = 'SBT' 18 | self._update_highlight_args() 19 | settings.add_on_change(self._update_highlight_args) 20 | 21 | def set_status_message(self, view, message): 22 | if message: 23 | view.set_status(self.status_key, message) 24 | else: 25 | view.erase_status(self.status_key) 26 | 27 | def clear(self, view): 28 | view.erase_regions(self.bookmark_key) 29 | for error_type in type(self).error_types: 30 | view.erase_regions(self.region_key(error_type)) 31 | 32 | def highlight(self, view, errors, replace=False): 33 | bookmarked_line = self._bookmark_error(view) 34 | grouped = group_by(errors, lambda e: e.error_type) 35 | for error_type in type(self).error_types: 36 | lines = [e.line for e in grouped.get(error_type, list())] 37 | lines = [l for l in lines if l != bookmarked_line] 38 | self._highlight_lines(view, lines, error_type, replace) 39 | 40 | def region_key(self, error_type): 41 | return 'sublimesbt_%s_marking' % error_type 42 | 43 | def region_scope(self, error_type): 44 | return self._mark_settings(error_type)['scope'] 45 | 46 | def _bookmark_error(self, view): 47 | for error in maybe(self._current_error_in_view(view)): 48 | region = self._create_region(view, error.line) 49 | self._clear_highlight(view, region) 50 | view.add_regions(self.bookmark_key, 51 | [region], 52 | self.region_scope(error.error_type), 53 | *self._bookmark_args(error.error_type)) 54 | return error.line 55 | 56 | def _highlight_lines(self, view, lines, error_type, replace): 57 | regions = self._all_regions(view, self._create_regions(view, lines), error_type, replace) 58 | self._highlight_regions(view, regions, error_type) 59 | 60 | def _highlight_regions(self, view, regions, error_type): 61 | view.add_regions(self.region_key(error_type), 62 | regions, 63 | self.region_scope(error_type), 64 | *self._highlight_args[error_type]) 65 | 66 | def _clear_highlight(self, view, region): 67 | for error_type in type(self).error_types: 68 | regions = view.get_regions(self.region_key(error_type)) 69 | if region in regions: 70 | regions = [r for r in regions if r != region] 71 | self._highlight_regions(view, regions, error_type) 72 | 73 | def _all_regions(self, view, new_regions, error_type, replace): 74 | if replace: 75 | return new_regions 76 | else: 77 | return view.get_regions(self.region_key(error_type)) + new_regions 78 | 79 | def _create_regions(self, view, lines): 80 | return [self._create_region(view, l) for l in lines] 81 | 82 | def _create_region(self, view, lineno): 83 | line = view.line(view.text_point(lineno - 1, 0)) 84 | r = view.find(r'\S', line.begin()) 85 | if r is not None and line.contains(r): 86 | return sublime.Region(r.begin(), line.end()) 87 | else: 88 | return line 89 | 90 | def _bookmark_args(self, error_type): 91 | return ['bookmark', self._highlight_args[error_type][-1]] 92 | 93 | def _update_highlight_args(self): 94 | self._highlight_args = { 95 | 'error': self._create_highlight_args('error'), 96 | 'failure': self._create_highlight_args('failure'), 97 | 'warning': self._create_highlight_args('warning') 98 | } 99 | 100 | def _create_highlight_args(self, error_type): 101 | style = self._mark_settings(error_type)['style'] 102 | if style == 'dot': 103 | return ['dot', sublime.HIDDEN] 104 | elif style == 'outline': 105 | return [sublime.DRAW_OUTLINED] 106 | else: 107 | return ['dot', sublime.DRAW_OUTLINED] 108 | 109 | def _mark_settings(self, error_type): 110 | return self.settings.get('%s_marking' % error_type) 111 | -------------------------------------------------------------------------------- /sbtview.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | try: 5 | from .sbtsettings import SBTSettings 6 | from .util import maybe, OnePerWindow 7 | except(ValueError): 8 | from sbtsettings import SBTSettings 9 | from util import maybe, OnePerWindow 10 | 11 | import re 12 | 13 | 14 | class SbtView(OnePerWindow): 15 | 16 | settings = { 17 | "line_numbers": False, 18 | "gutter": False, 19 | "rulers": [], 20 | "word_wrap": False, 21 | "draw_centered": False, 22 | "highlight_line": False 23 | } 24 | 25 | @classmethod 26 | def is_sbt_view(cls, view): 27 | if view is not None: 28 | for window in maybe(view.window()): 29 | sbt_view = cls(window) 30 | return sbt_view.panel.id() == view.id() 31 | 32 | def __init__(self, window): 33 | self.window = window 34 | self.settings = SBTSettings(window) 35 | self.panel = self.window.get_output_panel('sbt') 36 | self.panel.set_syntax_file("Packages/SublimeSBT/SBTOutput.hidden-tmLanguage") 37 | for name, setting in SbtView.settings.items(): 38 | self.panel.settings().set(name, setting) 39 | self._update_panel_colors() 40 | self.settings.add_on_change(self._update_panel_colors) 41 | self._output_size = 0 42 | self._set_running(False) 43 | 44 | def start(self): 45 | self.clear_output() 46 | self.show() 47 | self._set_running(True) 48 | 49 | def finish(self): 50 | self.show_output('\n -- Finished --\n') 51 | self._set_running(False) 52 | 53 | def show(self): 54 | self._update_panel_colors() 55 | self.window.run_command('show_panel', {'panel': 'output.sbt'}) 56 | sublime.set_timeout(self._show_selection, 0) 57 | 58 | def hide(self): 59 | self.window.run_command('hide_panel', {'panel': 'output.sbt'}) 60 | 61 | def focus(self): 62 | self.window.focus_view(self.panel) 63 | self.panel.show(self.panel.size()) 64 | 65 | def show_output(self, output): 66 | output = self._clean_output(output) 67 | self.show() 68 | self._append_output(output) 69 | self._output_size = self.panel.size() 70 | self.panel.show(self._output_size) 71 | self.panel.sel().clear() 72 | self.panel.sel().add(sublime.Region(self._output_size, self._output_size)) 73 | 74 | def clear_output(self): 75 | self._erase_output(sublime.Region(0, self.panel.size())) 76 | 77 | def take_input(self): 78 | input_region = sublime.Region(self._output_size, self.panel.size()) 79 | input = self.panel.substr(input_region) 80 | if sublime.platform() == 'windows': 81 | self._append_output('\n') 82 | else: 83 | self._erase_output(input_region) 84 | return input 85 | 86 | def delete_left(self): 87 | if self.panel.sel()[0].begin() > self._output_size: 88 | self.panel.run_command('left_delete') 89 | 90 | def delete_bol(self): 91 | if self.panel.sel()[0].begin() >= self._output_size: 92 | p = self.panel.sel()[-1].end() 93 | self._erase_output(sublime.Region(self._output_size, p)) 94 | 95 | def delete_word_left(self): 96 | if self.panel.sel()[0].begin() > self._output_size: 97 | for r in self.panel.sel(): 98 | p = max(self.panel.word(r).begin(), self._output_size) 99 | self.panel.sel().add(sublime.Region(p, r.end())) 100 | self._erase_output(*self.panel.sel()) 101 | 102 | def delete_word_right(self): 103 | if self.panel.sel()[0].begin() >= self._output_size: 104 | for r in self.panel.sel(): 105 | p = self.panel.word(r).end() 106 | self.panel.sel().add(sublime.Region(r.begin(), p)) 107 | self._erase_output(*self.panel.sel()) 108 | 109 | def update_writability(self): 110 | self.panel.set_read_only(not self._running or 111 | self.panel.sel()[0].begin() < self._output_size) 112 | 113 | def _set_running(self, running): 114 | self._running = running 115 | self.update_writability() 116 | 117 | def _append_output(self, output): 118 | self._run_command('sbt_append_output', output=output) 119 | 120 | def _erase_output(self, *regions): 121 | self._run_command('sbt_erase_output', 122 | regions=[[r.begin(), r.end()] for r in regions]) 123 | 124 | def _run_command(self, name, **kwargs): 125 | self.panel.set_read_only(False) 126 | self.panel.run_command(name, kwargs) 127 | self.update_writability() 128 | 129 | def _clean_output(self, output): 130 | return self._strip_codes(self._normalize_lines(output)) 131 | 132 | def _normalize_lines(self, output): 133 | return output.replace('\r\n', '\n').replace('\033M', '\r') 134 | 135 | def _show_selection(self): 136 | self.panel.show(self.panel.sel()[0].begin(), True) 137 | 138 | def _strip_codes(self, output): 139 | return re.sub(r'\033\[[0-9;]+[mK]', '', output) 140 | 141 | def _update_panel_colors(self): 142 | self.panel.settings().set('color_scheme', self.settings.get('color_scheme')) 143 | 144 | 145 | class SbtAppendOutputCommand(sublime_plugin.TextCommand): 146 | 147 | def run(self, edit, output): 148 | for i, s in enumerate(output.split('\r')): 149 | if i > 0: 150 | self.view.replace(edit, self.view.line(self.view.size()), s) 151 | else: 152 | self.view.insert(edit, self.view.size(), s) 153 | 154 | 155 | class SbtEraseOutputCommand(sublime_plugin.TextCommand): 156 | 157 | def run(self, edit, regions): 158 | for a, b in reversed(regions): 159 | self.view.erase(edit, sublime.Region(int(a), int(b))) 160 | -------------------------------------------------------------------------------- /outputmon.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .sbterror import SbtError 3 | from .util import maybe 4 | except(ValueError): 5 | from sbterror import SbtError 6 | from util import maybe 7 | 8 | import re 9 | 10 | 11 | class BuildOutputMonitor(object): 12 | 13 | def __init__(self, project): 14 | self.project = project 15 | self._parsers = [ErrorParser, TestFailureParser, MultilineTestFailureParser, 16 | FinishedParser] 17 | self._parser = None 18 | self._buffer = '' 19 | 20 | def __call__(self, output): 21 | lines = re.split(r'(?:\r\n|\n|\r)', self._buffer + output) 22 | self._buffer = lines[-1] 23 | for line in lines[0:-1]: 24 | self._output_line(self._strip_terminal_codes(line)) 25 | 26 | def _output_line(self, line): 27 | if self._parser: 28 | self._parser = self._parser.parse(line) 29 | else: 30 | self._parser = self._start_parsing(line) 31 | 32 | def _start_parsing(self, line): 33 | for parser_class in self._parsers: 34 | for parser in parser_class.start(self.project, line): 35 | return parser 36 | 37 | def _strip_terminal_codes(self, line): 38 | return re.sub(r'\033(?:M|\[[0-9;]+[mK])', '', line) 39 | 40 | 41 | class OutputParser(object): 42 | 43 | def parse(self, line): 44 | self.finish() 45 | 46 | 47 | class AbstractErrorParser(OutputParser): 48 | 49 | def __init__(self, project, line, filename, lineno, message): 50 | self.project = project 51 | self.reporter = project.error_reporter 52 | self.filename = filename 53 | self.lineno = lineno 54 | self.message = message 55 | self.extra_lines = [] 56 | 57 | def finish(self): 58 | self.reporter.error(self._error()) 59 | 60 | def _extra_line(self, line): 61 | self.extra_lines.append(line) 62 | 63 | def _error(self): 64 | return SbtError(project=self.project, 65 | filename=self.filename, 66 | line=self.lineno, 67 | message=self.message, 68 | error_type=self.error_type, 69 | extra_lines=self.extra_lines) 70 | 71 | 72 | class ErrorParser(AbstractErrorParser): 73 | 74 | @classmethod 75 | def start(cls, project, line): 76 | for m in maybe(re.match(r'^\[(error|warn)\]\s+(.+?):(\d+):(?:(\d+):)?\s+(.+)$', line)): 77 | yield cls(project, 78 | line=line, 79 | label=m.group(1), 80 | filename=m.group(2), 81 | lineno=int(m.group(3)), 82 | message=m.group(5)) 83 | 84 | def __init__(self, project, line, label, filename, lineno, message): 85 | AbstractErrorParser.__init__(self, project, line, filename, lineno, message) 86 | if label == 'warn': 87 | self.error_type = 'warning' 88 | else: 89 | self.error_type = 'error' 90 | 91 | def parse(self, line): 92 | for t in maybe(self._match_last_line(line)): 93 | self._extra_line(t) 94 | return self.finish() 95 | for t in maybe(self._match_line(line)): 96 | self._extra_line(t) 97 | return self 98 | return self.finish() 99 | 100 | def _match_last_line(self, line): 101 | for m in maybe(re.match(r'\[(?:error|warn)\] (\s*\^\s*)$', line)): 102 | return m.group(1) 103 | 104 | def _match_line(self, line): 105 | for m in maybe(re.match(r'\[(?:error|warn)\] (.*)$', line)): 106 | return m.group(1) 107 | 108 | 109 | class TestFailureParser(AbstractErrorParser): 110 | 111 | # Single line failures of the form: 112 | # [error|info] ... (filename::nn) 113 | 114 | @classmethod 115 | def start(cls, project, line): 116 | for m in maybe(re.match(r'\[(?:error|info)\]\s+(.+)\s+\(([^:]+):(\d+)\)$', line)): 117 | yield cls(project, 118 | line=line, 119 | filename=m.group(2), 120 | lineno=int(m.group(3)), 121 | message=m.group(1)) 122 | 123 | def __init__(self, project, line, filename, lineno, message): 124 | AbstractErrorParser.__init__(self, project, line, filename, lineno, message) 125 | self.error_type = 'failure' 126 | 127 | 128 | class MultilineTestFailureParser(AbstractErrorParser): 129 | 130 | # Multi-line failures of the form: 131 | # [info] - test description here *** FAILED *** 132 | # [info] ... 133 | # [info] ... (filename:nn) 134 | 135 | @classmethod 136 | def start(cls, project, line): 137 | for m in maybe(re.match(r'\[info\] - (.+) \*\*\* FAILED \*\*\*$', line)): 138 | yield cls(project, 139 | line=line, 140 | message=m.group(1)) 141 | 142 | def __init__(self, project, line, message): 143 | AbstractErrorParser.__init__(self, project, line, "dummy", 0, message) 144 | self.error_type = 'error' 145 | 146 | def parse(self, line): 147 | for (t, filename, lineno) in maybe(self._match_last_line(line)): 148 | self._extra_line(t) 149 | self.filename = filename 150 | self.lineno = lineno 151 | return self.finish() 152 | for t in maybe(self._match_line(line)): 153 | self._extra_line(t) 154 | return self 155 | return self.finish() 156 | 157 | def _match_last_line(self, line): 158 | for m in maybe(re.match(r'\[info\] (.+) \(([^:]+):(\d+)\)$', line)): 159 | return (m.group(1), m.group(2), int(m.group(3))) 160 | 161 | def _match_line(self, line): 162 | for m in maybe(re.match(r'\[info\] (.*)$', line)): 163 | return m.group(1) 164 | 165 | 166 | class FinishedParser(OutputParser): 167 | 168 | @classmethod 169 | def start(cls, project, line): 170 | if re.match(r'\[(?:success|error)\] Total time:', line): 171 | yield cls(project) 172 | 173 | def __init__(self, project): 174 | self.reporter = project.error_reporter 175 | 176 | def finish(self): 177 | self.reporter.finish() 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SublimeSBT 2 | ========== 3 | Scala [SBT](http://www.scala-sbt.org/) build tool integration for 4 | [Sublime Text 2](http://sublimetext.com/2) and 5 | [Sublime Text 3](http://sublimetext.com/3). 6 | 7 | Features 8 | -------- 9 | - Compatible with Sublime Text 2 and Sublime Text 3. 10 | - Runs SBT as an interactive REPL inside Sublime Text's output panel. 11 | - Detects compile errors and test failures in SBT's output and highlights the 12 | errant lines in the source code. 13 | - Lists compile errors and test failures in a quick panel for easy navigation 14 | to the errant lines. 15 | - Cycles through errors for even faster navigation. 16 | - Displays the error text in an output panel for easy reading. 17 | - Detects Play Framework projects and runs SBT using `play` instead of `sbt`. 18 | - Supports project-specific configuration. 19 | 20 | Installing 21 | ---------- 22 | Download [Package Control](http://wbond.net/sublime_packages/package_control) 23 | and use the *Package Control: Install Package* command from the command palette. 24 | Using Package Control ensures SublimeSBT will stay up to date automatically. 25 | 26 | Using 27 | ----- 28 | SublimeSBT is mostly used through the Command Palette. To open the pallete, 29 | press `ctrl+shift+p` (Windows, Linux) or `cmd+shift+p` (OS X). The SublimeSBT 30 | commands are all prefixed with `SBT:`. The commands only show up in the command 31 | palette if SublimeSBT detects that your project is an SBT project. 32 | 33 | **Start SBT** 34 | 35 | - Start an SBT session for the current project. If the project looks like a 36 | Play Framework project, the `play` command is used instead of the `sbt` 37 | command. 38 | 39 | **Stop SBT** 40 | 41 | - Stop the currently running SBT session. 42 | 43 | **Kill SBT** 44 | 45 | - Force the currently running SBT session to stop. 46 | 47 | **Show SBT Output** 48 | 49 | - Show the SBT output panel if it's not already showing. This also focuses 50 | the output panel and puts the cursor at the end. 51 | 52 | **Compile, Test, Run, Package, Start Console** 53 | 54 | - Run the `compile`, `test`, `run`, `package`, or `console` SBT command. If 55 | SBT is currently running the command is run in interactive mode, otherwise 56 | it's run in batch mode. 57 | 58 | **Start Continuous Compiling, Start Continuous Testing** 59 | 60 | - Run `~ compile` or `~ test`. If SBT is currently running the command is run 61 | in interactive mode, otherwise it's run in batch mode. 62 | 63 | **Test-Only, Test-Quick** 64 | 65 | - Run the `test-only` or `test-quick` command, prompting for an argument. If 66 | SBT is currently running the command is run in interactive mode, otherwise 67 | it's run in batch mode. 68 | 69 | **Start Continuous Test-Only, Start Continuous Test-Quick** 70 | 71 | - Run `~ test-only` or `~ test-quick`, prompting for an argument. If SBT is 72 | currently running the command is run in interactive mode, otherwise it's 73 | run in batch mode. 74 | 75 | **Reload** 76 | 77 | - Run the `reload` command if SBT is currently running. 78 | 79 | **Show Error List** 80 | 81 | - Show a quick panel with any compile errors or test failures. Selecting an 82 | error opens the file at the line with the error and shows the error text in 83 | the error output panel. 84 | 85 | **Show Next Error** 86 | 87 | - Jump to the next error in the error list. Opens the file at the line with 88 | the error and shows the error text in the error output panel. 89 | 90 | **Show Error Output** 91 | 92 | - Show the error output panel if it's not already showing. 93 | 94 | **Clear Errors** 95 | 96 | - Clear any compile errors or test failures and remove any highlighting 97 | thereof. 98 | 99 | **Show History** 100 | 101 | - Show a quick panel with the history of submitted commands. Selecting a 102 | command submits it again. 103 | 104 | **Show History and Edit** 105 | 106 | - As for Show History but also provides an opportunity to edit the selected 107 | command before it is re-submitted. 108 | 109 | **Clear History** 110 | 111 | - Clear the command history. 112 | 113 | Configuring 114 | ----------- 115 | The default settings can be viewed by accessing the ***Preferences > 116 | Package Settings > SublimeSBT > Settings – Default*** menu entry. To ensure 117 | settings are not lost when the package is upgraded, make sure all edits are 118 | saved to ***Settings – User***. 119 | 120 | **sbt_command** 121 | 122 | - An array representing the command line to use to start sbt. Depending on 123 | your setup you may need to put the full path to the file here. 124 | 125 | **play_command** 126 | 127 | - An array representing the command line to use to start sbt for a Play 128 | Framework project. Depending on your setup you may need to put the full 129 | path to the file here. 130 | 131 | **test_command** 132 | 133 | - A string representing the sbt command to use to run tests. 134 | 135 | **run_command** 136 | 137 | - A string representing the sbt command to use to run the project. 138 | 139 | **error\_marking, failure\_marking, warning\_marking** 140 | 141 | - How to mark errors, failures, and warnings in the source code: 142 | 143 | **style** 144 | 145 | - The mark style to use. Valid values are "dot", "outline", or "both". 146 | 147 | **scope** 148 | 149 | - The scope to use to color the outline. 150 | 151 | **color_scheme** 152 | 153 | - The color scheme to use for the output panel. 154 | 155 | Project-specific settings can be configured by accessing the ***Project > Edit 156 | Project*** menu entry and putting settings in a "SublimeSBT" object inside of 157 | "settings" in your project file, e.g.: 158 | 159 | { 160 | "folders": 161 | [ 162 | { 163 | "path": "/Users/jarhart/scalakoansexercises" 164 | } 165 | ], 166 | "settings": 167 | { 168 | "SublimeSBT": 169 | { 170 | "sbt_command": ["./sbt"], 171 | "test_command": "test-only org.functionalkoans.forscala.Koans" 172 | } 173 | } 174 | } 175 | 176 | **history** 177 | 178 | - An array that contains commands that should be added to the command 179 | history when a project is opened. 180 | 181 | **history_length** 182 | 183 | - The maximum number of unique commands to keep in the command history. 184 | The default setting is 20. 185 | 186 | Project-Specific Configuring 187 | ---------------------------- 188 | If the file `.SublimeSBT_history` exists at the top level of a project, each 189 | line of that file will be added to the command history when the project is 190 | opened. 191 | 192 | Contributors 193 | ------------ 194 | - [Jason Arhart](https://github.com/jarhart) 195 | - [Alexey Alekhin](https://github.com/laughedelic) 196 | - [Colt Frederickson](https://github.com/coltfred) 197 | - [Bryan Head](https://github.com/qiemem) 198 | - [Tony Sloane](https://github.com/inkytonik) 199 | - [Tim Gautier](https://github.com/timgautier) 200 | 201 | Contributing 202 | ------------ 203 | 204 | 1. Fork it 205 | 2. Create your feature branch (`git checkout -b my-new-feature`) 206 | 3. Commit your changes (`git commit -am 'Added some feature'`) 207 | 4. Push to the branch (`git push origin my-new-feature`) 208 | 5. Create new Pull Request 209 | 210 | Copyright 211 | --------- 212 | 213 | Copyright (c) 2012 Jason Arhart. See LICENSE for further details. 214 | -------------------------------------------------------------------------------- /sbtrunner.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | try: 4 | from .project import Project 5 | from .util import OnePerWindow 6 | except(ValueError): 7 | from project import Project 8 | from util import OnePerWindow 9 | 10 | import os 11 | import pipes 12 | import signal 13 | import subprocess 14 | import threading 15 | 16 | 17 | class SbtRunner(OnePerWindow): 18 | 19 | @classmethod 20 | def is_sbt_running_for(cls, window): 21 | return cls(window).is_sbt_running() 22 | 23 | def __init__(self, window): 24 | self._project = Project(window) 25 | self._proc = None 26 | self.init_history() 27 | 28 | def project_root(self): 29 | return self._project.project_root() 30 | 31 | def sbt_command(self, command): 32 | cmdline = self._project.sbt_command() 33 | if command is not None: 34 | cmdline.append(command) 35 | return cmdline 36 | 37 | def start_sbt(self, command, on_start, on_stop, on_stdout, on_stderr): 38 | if self.project_root() and not self.is_sbt_running(): 39 | self._proc = self._try_start_sbt_proc(self.sbt_command(command), 40 | on_start, 41 | on_stop, 42 | on_stdout, 43 | on_stderr) 44 | 45 | def stop_sbt(self): 46 | if self.is_sbt_running(): 47 | self._proc.terminate() 48 | 49 | def kill_sbt(self): 50 | if self.is_sbt_running(): 51 | self._proc.kill() 52 | 53 | def is_sbt_running(self): 54 | return (self._proc is not None) and self._proc.is_running() 55 | 56 | def send_to_sbt(self, input): 57 | if self.is_sbt_running(): 58 | self.add_to_history(input) 59 | self._proc.send(input) 60 | 61 | def _try_start_sbt_proc(self, cmdline, *handlers): 62 | try: 63 | return SbtProcess.start(cmdline, 64 | self.project_root(), 65 | self._project.settings, 66 | *handlers) 67 | except OSError: 68 | msg = ('Unable to find "%s".\n\n' 69 | 'You may need to specify the full path to your sbt command.' 70 | % cmdline[0]) 71 | sublime.error_message(msg) 72 | 73 | def init_history(self): 74 | self._history = [] 75 | if self.project_root(): 76 | path = os.path.join(self.project_root(), '.SublimeSBT_history') 77 | if os.path.exists(path): 78 | for line in open(path): 79 | self._history.append(line.rstrip('\n\r')) 80 | global_cmds = self._project.settings.get('history') or [] 81 | for cmd in global_cmds: 82 | self._history.append(cmd) 83 | 84 | def add_to_history(self, input): 85 | if input != '' and not input.isspace (): 86 | input = input.rstrip('\n\r') 87 | self._history = [cmd for cmd in self._history if cmd != input] 88 | self._history.insert (0, input) 89 | history_length = self._project.settings.get('history_length') or 20 90 | del self._history[history_length:] 91 | 92 | def clear_history(self): 93 | self._history.clear () 94 | 95 | def get_history(self): 96 | return self._history 97 | 98 | 99 | class SbtProcess(object): 100 | 101 | @staticmethod 102 | def start(*args, **kwargs): 103 | if sublime.platform() == 'windows': 104 | return SbtWindowsProcess._start(*args, **kwargs) 105 | else: 106 | return SbtUnixProcess._start(*args, **kwargs) 107 | 108 | @classmethod 109 | def _start(cls, cmdline, cwd, settings, *handlers): 110 | return cls(cls._start_proc(cmdline, cwd, settings), settings, *handlers) 111 | 112 | @classmethod 113 | def _start_proc(cls, cmdline, cwd, settings): 114 | return cls._popen(cmdline, 115 | env=cls._sbt_env(settings), 116 | stdin=subprocess.PIPE, 117 | stdout=subprocess.PIPE, 118 | stderr=subprocess.PIPE, 119 | cwd=cwd) 120 | 121 | @classmethod 122 | def _sbt_env(cls, settings): 123 | return dict(list(os.environ.items()) + 124 | [cls._append_opts('SBT_OPTS', cls._sbt_opts(settings))]) 125 | 126 | @classmethod 127 | def _sbt_opts(cls, settings): 128 | return [ 129 | str('-Dfile.encoding=%s' % (settings.get('encoding') or 'UTF-8')) 130 | ] 131 | 132 | @classmethod 133 | def _append_opts(cls, name, opts): 134 | existing_opts = os.environ.get(name, None) 135 | if existing_opts: 136 | opts = [existing_opts] + opts 137 | return [name, ' '.join(opts)] 138 | 139 | def __init__(self, proc, settings, on_start, on_stop, on_stdout, on_stderr): 140 | self._proc = proc 141 | self._encoding = settings.get('encoding') or 'UTF-8' 142 | on_start() 143 | if self._proc.stdout: 144 | self._start_thread(self._monitor_output, 145 | (self._proc.stdout, on_stdout)) 146 | if self._proc.stderr: 147 | self._start_thread(self._monitor_output, 148 | (self._proc.stderr, on_stderr)) 149 | self._start_thread(self._monitor_proc, (on_stop,)) 150 | 151 | def is_running(self): 152 | return self._proc.returncode is None 153 | 154 | def send(self, input): 155 | self._proc.stdin.write(input.encode()) 156 | self._proc.stdin.flush() 157 | 158 | def _monitor_output(self, pipe, handle_output): 159 | while True: 160 | output = os.read(pipe.fileno(), 2 ** 15).decode(self._encoding) 161 | if output != "": 162 | handle_output(output) 163 | else: 164 | pipe.close() 165 | return 166 | 167 | def _monitor_proc(self, handle_stop): 168 | self._proc.wait() 169 | sublime.set_timeout(handle_stop, 0) 170 | 171 | def _start_thread(self, target, args): 172 | threading.Thread(target=target, args=args).start() 173 | 174 | 175 | class SbtUnixProcess(SbtProcess): 176 | 177 | @classmethod 178 | def _popen(cls, cmdline, **kwargs): 179 | return subprocess.Popen(cls._shell_cmdline(cmdline), 180 | preexec_fn=os.setpgrp, 181 | **kwargs) 182 | 183 | @classmethod 184 | def _shell_cmdline(cls, cmdline): 185 | shell = os.environ.get('SHELL', '/bin/bash') 186 | opts = '-ic' if shell.endswith('csh') else '-lic' 187 | cmd = ' '.join(map(pipes.quote, cmdline)) 188 | return [shell, opts, cmd] 189 | 190 | def terminate(self): 191 | os.killpg(self._proc.pid, signal.SIGTERM) 192 | 193 | def kill(self): 194 | os.killpg(self._proc.pid, signal.SIGKILL) 195 | 196 | 197 | class SbtWindowsProcess(SbtProcess): 198 | 199 | @classmethod 200 | def _popen(cls, cmdline, **kwargs): 201 | return subprocess.Popen(cmdline, shell=True, **kwargs) 202 | 203 | def terminate(self): 204 | self.kill() 205 | 206 | def kill(self): 207 | cmdline = ['taskkill', '/T', '/F', '/PID', str(self._proc.pid)] 208 | si = subprocess.STARTUPINFO() 209 | si.dwFlags = subprocess.STARTF_USESHOWWINDOW 210 | si.wShowWindow = subprocess.SW_HIDE 211 | subprocess.call(cmdline, startupinfo=si) 212 | -------------------------------------------------------------------------------- /sublimesbt.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | import sublime 3 | 4 | import re 5 | 6 | try: 7 | from .project import Project 8 | from .sbtrunner import SbtRunner 9 | from .sbtview import SbtView 10 | from .errorview import ErrorView 11 | from .outputmon import BuildOutputMonitor 12 | from .util import delayed, maybe 13 | except(ValueError): 14 | from project import Project 15 | from sbtrunner import SbtRunner 16 | from sbtview import SbtView 17 | from errorview import ErrorView 18 | from outputmon import BuildOutputMonitor 19 | from util import delayed, maybe 20 | 21 | class SbtWindowCommand(sublime_plugin.WindowCommand): 22 | 23 | def __init__(self, *args): 24 | super(SbtWindowCommand, self).__init__(*args) 25 | self._project = Project(self.window) 26 | self._runner = SbtRunner(self.window) 27 | self._sbt_view = SbtView(self.window) 28 | self._error_view = ErrorView(self.window) 29 | self._error_reporter = self._project.error_reporter 30 | self._error_report = self._project.error_report 31 | self._monitor_compile_output = BuildOutputMonitor(self._project) 32 | 33 | def is_sbt_project(self): 34 | return self._project.is_sbt_project() 35 | 36 | def is_play_project(self): 37 | return self._project.is_play_project() 38 | 39 | def is_sbt_running(self): 40 | return self._runner.is_sbt_running() 41 | 42 | def start_sbt(self, command=None): 43 | self._runner.start_sbt(command, 44 | on_start=self._sbt_view.start, 45 | on_stop=self._sbt_view.finish, 46 | on_stdout=self._on_stdout, 47 | on_stderr=self._on_stderr) 48 | 49 | def stop_sbt(self): 50 | self._runner.stop_sbt() 51 | 52 | def kill_sbt(self): 53 | self._runner.kill_sbt() 54 | 55 | def show_sbt(self): 56 | self._sbt_view.show() 57 | 58 | def hide_sbt(self): 59 | self._sbt_view.hide() 60 | 61 | def focus_sbt(self): 62 | self._sbt_view.focus() 63 | 64 | def take_input(self): 65 | return self._sbt_view.take_input() 66 | 67 | def send_to_sbt(self, cmd): 68 | self.window.run_command('clear_sbt_errors') 69 | self._runner.send_to_sbt(cmd) 70 | 71 | @delayed(0) 72 | def show_error(self, error): 73 | self._error_report.focus_error(error) 74 | self._error_reporter.show_errors() 75 | self._error_view.show_error(error) 76 | self.goto_error(error) 77 | 78 | @delayed(0) 79 | def goto_error(self, error): 80 | self.window.open_file(error.encoded_position(), sublime.ENCODED_POSITION) 81 | 82 | def show_error_output(self): 83 | self._error_view.show() 84 | 85 | def setting(self, name): 86 | return self._project.setting(name) 87 | 88 | def _on_stdout(self, output): 89 | self._monitor_compile_output(output) 90 | self._show_output(output) 91 | 92 | def _on_stderr(self, output): 93 | self._show_output(output) 94 | 95 | @delayed(0) 96 | def _show_output(self, output): 97 | output = self._work_around_JLine_bug(output) 98 | self._sbt_view.show_output(output) 99 | 100 | # If we have a single character, space, CR then we are probably seeing a 101 | # JLine bug which has inserted the space, CR at column 80 of a prompt 102 | # line. Delete the space and CR so that it doesn't mess up the display 103 | # of the prompt line (ie. hide the stuff before the CR). Just remove 104 | # the space, CR pair. 105 | def _work_around_JLine_bug(self, output): 106 | return re.sub(r'^(.) \r$', r'\1', output) 107 | 108 | 109 | class StartSbtCommand(SbtWindowCommand): 110 | 111 | def run(self): 112 | self.start_sbt() 113 | 114 | def is_enabled(self): 115 | return self.is_sbt_project() and not self.is_sbt_running() 116 | 117 | 118 | class StopSbtCommand(SbtWindowCommand): 119 | 120 | def run(self): 121 | self.stop_sbt() 122 | 123 | def is_enabled(self): 124 | return self.is_sbt_running() 125 | 126 | 127 | class KillSbtCommand(SbtWindowCommand): 128 | 129 | def run(self): 130 | self.kill_sbt() 131 | 132 | def is_enabled(self): 133 | return self.is_sbt_running() 134 | 135 | 136 | class ShowSbtCommand(SbtWindowCommand): 137 | 138 | def run(self): 139 | self.show_sbt() 140 | self.focus_sbt() 141 | 142 | def is_enabled(self): 143 | return self.is_sbt_project() 144 | 145 | 146 | class SbtSubmitCommand(SbtWindowCommand): 147 | 148 | def run(self): 149 | self.send_to_sbt(self.take_input() + '\n') 150 | 151 | def is_enabled(self): 152 | return self.is_sbt_running() 153 | 154 | 155 | class SbtCommand(SbtWindowCommand): 156 | 157 | def run(self, command): 158 | if self.is_sbt_running(): 159 | self.send_to_sbt(command + '\n') 160 | else: 161 | self.start_sbt(command) 162 | 163 | def is_enabled(self): 164 | return self.is_sbt_project() 165 | 166 | 167 | class SbtTestCommand(SbtCommand): 168 | 169 | def run(self): 170 | super(SbtTestCommand, self).run(self.test_command()) 171 | 172 | def test_command(self): 173 | return self.setting('test_command') 174 | 175 | 176 | class SbtContinuousTestCommand(SbtTestCommand): 177 | 178 | def test_command(self): 179 | return '~ ' + super(SbtContinuousTestCommand, self).test_command() 180 | 181 | 182 | test_only_arg = '*' 183 | 184 | 185 | class SbtTestOnlyCommand(SbtCommand): 186 | 187 | base_command = 'test-only' 188 | 189 | def run(self): 190 | self.window.show_input_panel(self.prompt(), test_only_arg, 191 | self.test_only, None, None) 192 | 193 | def test_only(self, arg): 194 | global test_only_arg 195 | test_only_arg = arg 196 | super(SbtTestOnlyCommand, self).run(self.test_command(arg)) 197 | 198 | def prompt(self): 199 | return 'SBT: %s' % self.base_command 200 | 201 | def test_command(self, arg): 202 | return '%s %s' % (self.base_command, arg) 203 | 204 | 205 | class SbtContinuousTestOnlyCommand(SbtTestOnlyCommand): 206 | 207 | def test_command(self, arg): 208 | return '~ ' + super(SbtContinuousTestOnlyCommand, self).test_command(arg) 209 | 210 | 211 | class SbtTestQuickCommand(SbtTestOnlyCommand): 212 | 213 | base_command = 'test-quick' 214 | 215 | 216 | class SbtContinuousTestQuickCommand(SbtTestQuickCommand): 217 | 218 | def test_command(self, arg): 219 | return '~ ' + super(SbtContinuousTestQuickCommand, self).test_command(arg) 220 | 221 | 222 | class SbtRunCommand(SbtCommand): 223 | 224 | def run(self): 225 | super(SbtRunCommand, self).run(self.setting('run_command')) 226 | 227 | 228 | class SbtReloadCommand(SbtCommand): 229 | 230 | def run(self): 231 | super(SbtReloadCommand, self).run('reload') 232 | 233 | def is_enabled(self): 234 | return self.is_sbt_running() 235 | 236 | 237 | class SbtErrorsCommand(SbtWindowCommand): 238 | 239 | def is_enabled(self): 240 | return self.is_sbt_project() and self._error_report.has_errors() 241 | 242 | 243 | class ClearSbtErrorsCommand(SbtErrorsCommand): 244 | 245 | def run(self): 246 | self._error_reporter.clear() 247 | self._error_view.clear() 248 | 249 | 250 | class ListSbtErrorsCommand(SbtErrorsCommand): 251 | 252 | def run(self): 253 | errors = list(self._error_report.all_errors()) 254 | list_items = [e.list_item() for e in errors] 255 | 256 | def goto_error(index): 257 | if index >= 0: 258 | self.show_error(errors[index]) 259 | 260 | self.window.show_quick_panel(list_items, goto_error) 261 | 262 | 263 | class NextSbtErrorCommand(SbtErrorsCommand): 264 | 265 | def run(self): 266 | self.show_error(self._error_report.next_error()) 267 | 268 | 269 | class ShowSbtErrorOutputCommand(SbtErrorsCommand): 270 | 271 | def run(self): 272 | self.show_error_output() 273 | 274 | 275 | class SbtEotCommand(SbtWindowCommand): 276 | 277 | def run(self): 278 | if sublime.platform() == 'windows': 279 | self.send_to_sbt('\032') 280 | else: 281 | self.send_to_sbt('\004') 282 | 283 | def is_enabled(self): 284 | return self.is_sbt_running() 285 | 286 | 287 | class SbtDeleteLeftCommand(SbtWindowCommand): 288 | 289 | def run(self): 290 | self._sbt_view.delete_left() 291 | 292 | 293 | class SbtDeleteBolCommand(SbtWindowCommand): 294 | 295 | def run(self): 296 | self._sbt_view.delete_bol() 297 | 298 | 299 | class SbtDeleteWordLeftCommand(SbtWindowCommand): 300 | 301 | def run(self): 302 | self._sbt_view.delete_word_left() 303 | 304 | 305 | class SbtDeleteWordRightCommand(SbtWindowCommand): 306 | 307 | def run(self): 308 | self._sbt_view.delete_word_right() 309 | 310 | 311 | class SbtShowHistoryCommand(SbtWindowCommand): 312 | 313 | def run(self, editable=False): 314 | def get_command(index): 315 | if index >= 0: 316 | cmd = self._runner.get_history()[index] 317 | if editable: 318 | self.window.show_input_panel("Command:", cmd, 319 | self.run_command, 320 | None, None) 321 | else: 322 | self.run_command(cmd) 323 | history = self._runner.get_history() 324 | if history == []: 325 | sublime.error_message('There is no SBT command history to display.') 326 | else: 327 | self.window.show_quick_panel(history, get_command) 328 | 329 | def run_command(self, cmd): 330 | self.send_to_sbt (cmd + '\n') 331 | 332 | def is_enabled(self): 333 | return self.is_sbt_running() 334 | 335 | 336 | class SbtClearHistoryCommand(SbtWindowCommand): 337 | 338 | def run(self): 339 | self._runner.clear_history() 340 | 341 | 342 | class SbtListener(sublime_plugin.EventListener): 343 | 344 | def on_clone(self, view): 345 | for reporter in maybe(self._reporter(view)): 346 | reporter.show_errors_in(view.file_name()) 347 | 348 | def on_load(self, view): 349 | for reporter in maybe(self._reporter(view)): 350 | reporter.show_errors_in(view.file_name()) 351 | 352 | def on_post_save(self, view): 353 | for reporter in maybe(self._reporter(view)): 354 | reporter.hide_errors_in(view.file_name()) 355 | 356 | def on_modified(self, view): 357 | for reporter in maybe(self._reporter(view)): 358 | reporter.show_errors_in(view.file_name()) 359 | 360 | def on_selection_modified(self, view): 361 | if SbtView.is_sbt_view(view): 362 | SbtView(view.window()).update_writability() 363 | else: 364 | for reporter in maybe(self._reporter(view)): 365 | reporter.update_status_now() 366 | 367 | def on_activated(self, view): 368 | for reporter in maybe(self._reporter(view)): 369 | reporter.show_errors_in(view.file_name()) 370 | 371 | def on_query_context(self, view, key, operator, operand, match_all): 372 | if key == "in_sbt_view": 373 | if SbtView.is_sbt_view(view): 374 | return SbtRunner.is_sbt_running_for(view.window()) 375 | else: 376 | return False 377 | 378 | def _reporter(self, view): 379 | for window in maybe(view.window()): 380 | return Project(window).error_reporter 381 | --------------------------------------------------------------------------------