diff --git a/Build Systems/Elm Make.sublime-build b/Build Systems/Elm Make.sublime-build index 176de84..1704606 100644 --- a/Build Systems/Elm Make.sublime-build +++ b/Build Systems/Elm Make.sublime-build @@ -3,67 +3,14 @@ "selector": "source.elm", "cmd": [ - "elm-make", + "{elm_binary}", + "make", "$file", - "--output={null}", - "--report=json", - "--yes" + "--output=/dev/null", + "--report=json" ], - "working_dir": "$project_path", - "file_regex": "^\\-\\- \\w+: (?=.+ \\- (.+?):(\\d+):(\\d+))(.+) \\- .*$", - "error_format": "-- $type: $tag - $file:$line:$column\n$message\n", - "info_format": ":: $info\n", - "syntax": "Packages/Elm Language Support/Syntaxes/Elm Compile Messages.sublime-syntax", - "null_device": "/dev/null", - "warnings": "true", - "osx": + "cancel": { - "path": "/usr/local/bin:$PATH" - }, - "linux": - { - "path": "$HOME/.cabal/bin:/usr/local/bin:$PATH" - }, - "variants": - [ - { - "name": "Run", - "cmd": - [ - "elm-make", - "$file", - "--output={output}", - "--report=json", - "--yes" - ] - }, - { - "name": "Ignore Warnings", - "warnings": "false" - }, - { - "name": "Run - debug", - "cmd": - [ - "elm-make", - "$file", - "--output={output}", - "--report=json", - "--debug", - "--yes" - ] - }, - { - "name": "Run - Ignore Warnings", - "warnings": "false", - "cmd": - [ - "elm-make", - "$file", - "--output={output}", - "--report=json", - "--yes" - ] - } - ] + "kill": true + } } diff --git a/Commands/Show Type.sublime-commands b/Commands/Show Type.sublime-commands deleted file mode 100644 index 056d045..0000000 --- a/Commands/Show Type.sublime-commands +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "caption": "Elm Language Support: Show type", - "command": "elm_show_type", - "args": { "panel": true } - }, - { - "caption": "Elm Language Support: Open type panel", - "command": "elm_show_type_panel" - } -] \ No newline at end of file diff --git a/Keymaps/Default.sublime-keymap b/Keymaps/Default.sublime-keymap deleted file mode 100644 index 4bff411..0000000 --- a/Keymaps/Default.sublime-keymap +++ /dev/null @@ -1,8 +0,0 @@ -[ - { "keys": ["alt+up"], "command": "elm_show_type_panel", - "context": - [ { "key": "selector", "operator": "equal", "operand": "source.elm" } ] - }, - { "keys": ["alt+down"], "command": "hide_panel" - } -] \ No newline at end of file diff --git a/Menus/Context.sublime-menu b/Menus/Context.sublime-menu deleted file mode 100644 index 82ebb89..0000000 --- a/Menus/Context.sublime-menu +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "id": "elmlanguagesupport", - "caption": "Elm Language Support", - "children": - [ - { - "caption": "Open Type Panel", - "command": "elm_show_type_panel" - } - ] - } -] \ No newline at end of file diff --git a/README.md b/README.md index 9a2af81..14478ce 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,6 @@ | type | ``type`` | | typea | ``type alias (Record)`` | -- Autocompletions plus type signature and documentation display for all functions inside packages in your `elm-package.json` file (requires [elm-oracle](https://www.npmjs.com/package/elm-oracle), which you can install with `npm install -g elm-oracle`) - 1. Bring up the type panel with `alt+up` or through the right-click context menu - 2. Close the type panel with `alt+down` - 3. If you don't like these keybindings, rebind them in your User packages directory -![autocompletions screenshot](images/completions.png)![type signature screenshot](images/elm_types.png)![type panel screenshot](images/type_panel.png) - Four standard build commands (Super+[Shift]+B or Super+[Shift]+F7) 1. `Build` just checks errors. Kudos to this [tweet][]! 2. `Run` additionally outputs your compiled program to an inferred path. @@ -51,7 +46,6 @@ 3. Compile message highlighting, embedded code highlighting, and color scheme for output panel. ![compile messages screenshot](images/elm_make.jpg) - Integration with popular plugins (installed separately) 1. [SublimeREPL][] — Run `elm-repl` in an editor tab with syntax highlighting. ![SublimeREPL screenshot](images/elm_repl.jpg) - 2. [Highlight Build Errors][] — Does what it says on the box … usually. - Integration with [elm format](https://github.com/avh4/elm-format) 1. Make sure `elm-format` is in your PATH 2. Run the "Elm Language Support: Run elm-format" command from the Command Palette to run elm-format on the current file diff --git a/Settings/Elm Language Support.sublime-settings b/Settings/Elm Language Support.sublime-settings index abc419f..9dd5ed2 100644 --- a/Settings/Elm Language Support.sublime-settings +++ b/Settings/Elm Language Support.sublime-settings @@ -1,9 +1,9 @@ { "debug": false, "enabled": true, + "elm_binary": "elm", "elm_format_binary": "elm-format", "elm_format_on_save": true, "elm_format_filename_filter": "", - "elm_paths": "", - "build_error_color_scheme": "Packages/Color Scheme - Default/Sunburst.tmTheme" + "elm_paths": "" } diff --git a/Settings/Elm User Strings.sublime-settings b/Settings/Elm User Strings.sublime-settings index 6fb25d4..3b5125b 100644 --- a/Settings/Elm User Strings.sublime-settings +++ b/Settings/Elm User Strings.sublime-settings @@ -1,8 +1,9 @@ { - "logging.prefix": "[Elm says]: ", + "logging.prefix": "Elm Language Support: ", "logging.missing_plugin": "Missing plugin: {0}", "make.missing_plugin": "To highlight build errors: Install with Package Control: Highlight Build Errors", + "make.logging.json": "JSON from elm-make: {0}", "make.logging.invalid_json": "Invalid JSON from elm-make: {0}", "open_in_browser.not_found": "HTML file NOT found to open: {0}", diff --git a/Syntaxes/Elm Compile Messages.sublime-syntax b/Syntaxes/Elm Compile Messages.sublime-syntax index 85327fb..cc667c0 100644 --- a/Syntaxes/Elm Compile Messages.sublime-syntax +++ b/Syntaxes/Elm Compile Messages.sublime-syntax @@ -5,8 +5,10 @@ name: Elm Compile Messages hidden: true file_extensions: [] scope: text.html.mediawiki.elm-build-output + contexts: main: + - meta_scope: meta.report.elm-build-output - match: "^(::) " comment: "|> Unparsed Compile Message" push: @@ -20,7 +22,7 @@ contexts: comment: Successfully generated scope: constant.language.boolean.true.elm-build-output - match: |- - (?x) # Minimally modified `file_regex` from `Elm Make.sublime-build` + (?x) # Minimally modified `file_regex` from `Elm Make.sublime-build` ^\-\-[ ] # Leading delimiter ((error) # \2: error |(warning) # \3: warning @@ -32,7 +34,7 @@ contexts: (\d+): # \6: $line (\d+) # \7: $column \n$ # End - comment: '-- TAG - file:line:column\nOverview\nDetail\n' + comment: '-- type: TAG - file:line:column\nMessage\n' captures: 0: markup.heading.4.elm-build-output 1: support.constant.type.elm-build-output @@ -42,31 +44,39 @@ contexts: 5: markup.underline.link.elm-build-output 6: constant.numeric.elm-build-output 7: constant.numeric.elm-build-output + - match: (`)(?!`) + comment: Inline `variable` + scope: punctuation.definition.raw.elm-build-output push: - - meta_scope: meta.report.elm-build-output - - meta_content_scope: string.unquoted.elm-build-output - - match: ^\n$ - captures: - 0: meta.separator.elm-build-output - pop: true - - match: (`)(?!`) - comment: Inline `variable` + - meta_scope: markup.raw.inline.elm-build-output + - meta_content_scope: variable.other.elm.elm-build-output + - match: \1 captures: 0: punctuation.definition.raw.elm-build-output - push: - - meta_scope: markup.raw.inline.elm-build-output - - meta_content_scope: variable.other.elm.elm-build-output - - match: \1 - captures: - 0: punctuation.definition.raw.elm-build-output - pop: true - - match: "(?m)^ {4}" - comment: Code Block - push: - - meta_scope: markup.raw.block.elm-build-output - - match: '\n+(?!^ {4})' - pop: true - - include: scope:source.elm + pop: true + - match: '(<)([^>]+)(>)' + comment: Inline + captures: + 1: punctuation.definition.link.elm-build-output + 2: markup.underline.link.elm-build-output + 3: punctuation.definition.link.elm-build-output + - match: '^Hint:' + scope: markup.heading.5.elm-build-output + - match: "^ +([x^]+)$" + comment: error jaggy underline + captures: + 1: punctuation.definition.raw.elm-build-output + - match: "^ {4}" + comment: code block + embed: Elm.sublime-syntax + escape: \n + - match: '^(\d+)(\|( |>))' + captures: + 1: constant.numeric.line-number.elm-build-output + 2: punctuation.definition.raw.elm-build-output + comment: line-numbered code block + embed: scope:source.elm + escape: \n - match: ^\[ comment: '[Finished in 4.2s]' push: diff --git a/beta-repository.json b/beta-repository.json deleted file mode 100644 index 18e5bf2..0000000 --- a/beta-repository.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "schema_version": "3.0.0", - "packages": [ - { - "name": "Elm Language Support", - "details": "https://github.com/deadfoxygrandpa/Elm.tmLanguage", - "releases": [ - { - "sublime_text": "*", - "branch": "beta" - } - ] - } - ], - "dependencies": [ - ], - "includes": [ - ] -} \ No newline at end of file diff --git a/elm_generate.py b/elm_generate.py deleted file mode 100644 index 2a97182..0000000 --- a/elm_generate.py +++ /dev/null @@ -1,165 +0,0 @@ -import json -import os -import sys - -class Module(object): - def __init__(self, data): - self.name = data['name'] - self.values = [name(v['raw']) + ' : ' + signature(v['raw']) for v in data['values']] - self.valueNames = [name(v) for v in self.values] - self.datatypes = [v['name'] for v in data['datatypes']] - self.constructors = [[v['name'] for v in x['constructors']] for x in data['datatypes']] - self.aliases = [v['name'] for v in data['aliases']] - - def include_text(self): - s = '\n\tinclude\n\t#{}\n'.format(self.name.lower()) - return s - - def moduleText(self): - s = '{nameLower}\n\n\tcaptures\n\t\n\t\t1\n\t\t\n\t\t\tname\n\t\t\tvariable.parameter\n\t\t\n\t\t2\n\t\t\n\t\t\tname\n\t\t\tvariable.parameter\n\t\t\n\t\t3\n\t\t\n\t\t\tname\n\t\t\tsupport.function.elm\n\t\t\n\t\n\tmatch\n\t\\b({name})(.)({values})\\b\n\tname\n\tvariable.parameter\n' - values = '|'.join([n for n in self.valueNames if not n.startswith('(')]) - if self.aliases: - values += '|' + '|'.join(self.aliases) - if self.datatypes: - values += '|' + '|'.join(self.datatypes) - return s.format(nameLower=self.name.lower(), name=self.name, values=values) - - def snippets(self): - base = 'Snippets' - s = '\n\t\n\t\n\t{name}\n\t\n\tsource.elm\n\t{signature}\n' - for v in [func for func in self.values if not name(func).startswith('(')]: - subdirectories = self.name.split('.') - path = '{}' + '\\{}'*(len(subdirectories)) - path = path.format(base, *subdirectories) - - if not os.path.exists(path): - os.makedirs(path) - - path += '\\{}' - - with open(path.format(name(v) + '.sublime-snippet'), 'w') as f: - f.write(s.format(autocomplete=make_autocomplete(v), name=name(v), signature=signature(v))) - - print('Wrote {}'.format(path.format(name(v) + '.sublime-snippet'))) - -def name(t): - return t.split(' : ')[0].strip() - -def signature(t): - return t.split(' : ')[1].strip() - -def hintize(t): - first = t[0].lower() - t = t.replace(' ', '') - return first + ''.join(t[1:]) - -def typeFormat(t): - if t[0] == '[': - return 'ListOf' + typeFormat(t[1:-1]) - elif t[0] == '(': - return ''.join([unicode(v.strip()) for v in t[1:-1].split(',')]) + 'Tuple' - else: - if len(t.split(' ')) == 1: - return t - else: - x = t.split(' ') - return x[0] + ''.join([typeFormat(v) for v in x[1:]]) - -def tokenize(t): - return [v.strip() for v in t.split('->')] - -def print_type(t): - print(name(t)) - print([typeFormat(v) for v in tokenize(signature(t))]) - -def make_autocomplete(t): - s = '{}'.format(name(t)) - args = arguments(signature(t)) - for n, arg in enumerate(args): - s += ' ${{{n}:{arg}}}'.format(n=n+1, arg=arg) - return s - -def arguments(signature): - args = [v.strip() for v in signature.split('->')][:-1] - new_args = [] - open_parens = 0 - for arg in args: - parens = arg.count('(') - arg.count(')') - if parens and not open_parens: - new_args.append('function') - elif open_parens != 0: - open_parens += parens - continue - else: - new_args.append(argify(arg)) - open_parens += parens - return new_args - -def argify(s): - if s.startswith('('): - return 'tuple' - elif s.startswith('['): - return 'list' - elif len(s.split(' ')) > 1: - return s.split(' ')[0].lower() - else: - return s.lower() - -def loadDocs(path): - with open(path) as f: - return json.load(f) - - -if __name__ == '__main__': - ## Usage: pass in docs.json from cabal's elm directory - path = sys.argv[1] - prelude = ['Basics', 'List', 'Signal', 'Text', 'Maybe', 'Time', 'Graphics.Element', 'Color', 'Graphics.Collage'] - - modules = [Module(m) for m in loadDocs(path)] - - print('Prelude:') - print('show|') - for m in modules: - if m.name in prelude: - print('|'.join([n for n in m.valueNames if not n.startswith('(')])) - - print('\n'*5) - - print('Prelude Aliases and Datatypes:') - print('Int|Float|Char|Bool|String|True|False') - for m in modules: - if m.name in prelude: - print('|'.join([n for n in (m.datatypes + m.aliases) if not n.startswith('(')]) + '|') - - print('\n'*5) - - print('Includes:') - for m in modules: - print(m.include_text()) - - print('\n'*5) - - print('Includes Continued:') - for m in modules: - print(m.moduleText()) - - print('\n'*5) - - print('Constructors:') - print('\(\)|\[\]|True|False|Int|Char|Bool|String|') - for m in modules: - if m.name in prelude: - for c in m.constructors: - print('|'.join(c) + '|') - - print('\n'*5) - - print('Writing Autocompletion Snippets...:') - for m in modules: - if m.name in prelude: - m.snippets() - print('\n'*2) - - with open('Snippets\\Basics\\markdown.sublime-snippet', 'w') as f: - f.write('\n\n\nmarkdown\n\nsource.elm\nA markdown block\n') - print('Wrote markdown.sublime-snippet') diff --git a/elm_make.py b/elm_make.py index cff5a23..e07237a 100644 --- a/elm_make.py +++ b/elm_make.py @@ -1,91 +1,277 @@ +import sublime +import sublime_plugin + +import html import json -import re +import os import string -import sublime +import subprocess +import threading + +from .elm_plugin import * +from .elm_project import ElmProject + +# We need a custom build command so that we can take the JSON output from the +# Elm compiler and render it in a format that works with Sublime Text’s build +# output panel syntax highlighting and regexp-based error navigation. +# +# Based on Advanced Example: https://www.sublimetext.com/docs/3/build_systems.html#advanced_example +class ElmMakeCommand(sublime_plugin.WindowCommand): + + encoding = 'utf-8' + killed = False + proc = None + panel = None + panel_lock = threading.Lock() + + errs_by_file = {} + phantom_sets_by_buffer = {} + show_errors_inline = True -try: # ST3 - from .elm_plugin import * - from .elm_project import ElmProject -except: # ST2 - from elm_plugin import * - from elm_project import ElmProject -default_exec = import_module('Default.exec') - -@replace_base_class('Highlight Build Errors.HighlightBuildErrors.ExecCommand') -class ElmMakeCommand(default_exec.ExecCommand): - - # inspired by: http://www.sublimetext.com/forum/viewtopic.php?t=12028 - def run(self, error_format, info_format, syntax, null_device, warnings, **kwargs): - self.buffer = '' - self.data_in_bytes = False # ST3 r3153 changed ExecCommand from bytes to str so we must detect which we get and handle appropriately: https://github.com/elm-community/SublimeElmLanguageSupport/issues/48 - self.warnings = warnings == "true" - self.error_format = string.Template(error_format) - self.info_format = string.Template(info_format) - self.run_with_project(null_device=null_device, **kwargs) - self.style_output(syntax) - - def run_with_project(self, cmd, working_dir, null_device, **kwargs): - file_arg, output_arg = cmd[1:3] - project = ElmProject(file_arg) + def is_enabled(self, kill=False): + # Cancel only available when the process is still running + if kill: + return self.proc is not None and self.proc.poll() is None + return True + + def run(self, cmd=[], kill=False): + if kill: + if self.proc: + self.killed = True + self.proc.terminate() + return + + working_dir = self.working_dir() + self.create_panel(working_dir) + + if self.proc is not None: + self.proc.terminate() + self.proc = None + + self.proc = subprocess.Popen( + self.format_cmd(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=working_dir + ) + self.killed = False + + threading.Thread( + target=self.read_handle, + args=(self.proc.stdout,) + ).start() + + def working_dir(self): + vars = self.window.extract_variables() + project = ElmProject(vars['file']) log_string('project.logging.settings', repr(project)) - if '{output}' in output_arg: - cmd[1] = fs.expanduser(project.main_path) - output_path = fs.expanduser(project.output_path) - cmd[2] = output_arg.format(output=output_path) - else: - # cmd[1] builds active file rather than project main - cmd[2] = output_arg.format(null=null_device) - project_dir = project.working_dir or working_dir - # ST2: TypeError: __init__() got an unexpected keyword argument 'syntax' - super(ElmMakeCommand, self).run(cmd, working_dir=project_dir, **kwargs) - - def style_output(self, syntax): - self.output_view.set_syntax_file(syntax) - elm_setting = sublime.load_settings('Elm Language Support.sublime-settings') - user_setting = sublime.load_settings('Preferences.sublime-settings') - color_scheme = elm_setting.get('build_error_color_scheme') or user_setting.get('color_scheme') - self.output_view.settings().set('color_scheme', color_scheme) - if self.is_patched: - self.debug_text = '' - else: - self.debug_text = get_string('make.missing_plugin') + return project.working_dir or vars['project_path'] or vars['file_path'] - def on_data(self, proc, data): - if isinstance(data, str): - self.buffer += data - else: - # ST3 r3153 changed ExecCommand from bytes to str so we must detect which we get and handle appropriately: https://github.com/elm-community/SublimeElmLanguageSupport/issues/48 - self.data_in_bytes = True - self.buffer += data.decode(self.encoding) - - def on_finished(self, proc): - result_strs = self.buffer.split('\n') - flat_map = lambda f, xss: sum(map(f, xss), []) - output_strs = flat_map(self.format_result, result_strs) + [''] - output_data = '\n'.join(output_strs) - # ST3 r3153 changed ExecCommand from bytes to str so we must detect which we get and handle appropriately: https://github.com/elm-community/SublimeElmLanguageSupport/issues/48 - output_data = output_data.encode(self.encoding) if self.data_in_bytes else output_data - super(ElmMakeCommand, self).on_data(proc, output_data) - super(ElmMakeCommand, self).on_finished(proc) - - def format_result(self, result_str): - decode_error = lambda dict: self.format_error(**dict) if 'type' in dict else dict + def create_panel(self, working_dir): + # Only allow one thread to touch output panel at a time + with self.panel_lock: + # implicitly clears previous contents + self.panel = self.window.create_output_panel('exec') + + settings = self.panel.settings() + + self.panel.assign_syntax('Packages/Elm Language Support/Syntaxes/Elm Compile Messages.sublime-syntax') + settings.set('gutter', False) + settings.set('scroll_past_end', False) + settings.set('word_wrap', False) + settings.set('color_scheme', self.get_setting('build_output_color_scheme', 'color_scheme')) + + # Enable result navigation + settings.set( + 'result_file_regex', + r'^\-\- \w+: (?=.+ \- (.+?):(\d+):(\d+))(.+) \- .*$' + ) + settings.set('result_base_dir', working_dir) + + preferences = sublime.load_settings('Preferences.sublime-settings') + + self.hide_phantoms() + self.show_errors_inline = preferences.get('show_errors_inline', True) + + show_panel_on_build = preferences.get('show_panel_on_build', True) + if show_panel_on_build: + self.window.run_command('show_panel', {'panel': 'output.exec'}) + + def format_cmd(self, cmd): + binary, command, file, output = cmd[0:4] + + binary = binary.format(elm_binary=self.get_setting('elm_binary')) + + return [binary, command, file, output] + cmd[4:] + + def read_handle(self, handle): + chunk_size = 2 ** 13 + output = b'' + while True: + try: + chunk = os.read(handle.fileno(), chunk_size) + output += chunk + + if chunk == b'': + if output != b'': + self.queue_write(self.format_output(output.decode(self.encoding))) + raise IOError('EOF') + + except UnicodeDecodeError as e: + msg = 'Error decoding output using %s - %s' + self.queue_write(msg % (self.encoding, str(e))) + break + + except IOError: + if self.killed: + msg = 'Cancelled' + else: + msg = 'Finished' + sublime.set_timeout(lambda: self.finish(), 0) + self.queue_write('[%s]' % msg) + break + + def queue_write(self, text): + # Calling set_timeout inside this function rather than inline ensures + # that the value of text is captured for the lambda to use, and not + # mutated before it can run. + sublime.set_timeout(lambda: self.do_write(text), 1) + + def do_write(self, text): + with self.panel_lock: + self.panel.set_read_only(False) + self.panel.run_command('append', {'characters': text}) + self.panel.set_read_only(True) + + if self.show_errors_inline and text.find('\n') >= 0: + errs = self.panel.find_all_results_with_text() + errs_by_file = {} + for file, line, column, text in errs: + if file not in errs_by_file: + errs_by_file[file] = [] + errs_by_file[file].append((line, column, text)) + self.errs_by_file = errs_by_file + + self.update_phantoms() + + def format_output(self, output): try: - data = json.loads(result_str, object_hook=decode_error) - return [s for s in data if s is not None] - except ValueError: - log_string('make.logging.invalid_json', result_str) - info_str = result_str.strip() - return [self.info_format.substitute(info=info_str)] if info_str else [] - - def format_error(shelf, type, file, region, tag, overview, details, **kwargs): - if type == 'warning' and not shelf.warnings: - return None - line = region['start']['line'] - column = region['start']['column'] - message = overview - if details: - message += '\n' + re.sub(r'(\n)+', r'\1', details) - # TypeError: substitute() got multiple values for argument 'self' - # https://bugs.python.org/issue23671 - return shelf.error_format.substitute(**locals()) + data = json.loads(output) + log_string('make.logging.json', output) + return self.format_errors(data['errors']) + except ValueError as e: + log_string('make.logging.invalid_json', output) + return '' + + def format_errors(self, errors): + return '\n'.join(map(self.format_error, errors)) + '\n' + + def format_error(self, error): + file = error['path'] + return '\n'.join(map(lambda problem: self.format_problem(file, problem), error['problems'])) + + def format_problem(self, file, problem): + error_format = string.Template('-- $type: $title - $file:$line:$column\n\n$message\n') + + type = 'error' + title = problem['title'] + line = problem['region']['start']['line'] + column = problem['region']['start']['column'] + message = self.format_message(problem['message']) + + vars = locals() + vars.pop('self') # https://bugs.python.org/issue23671 + return error_format.substitute(**vars) + + def format_message(self, message): + format = lambda msg: msg['string'] if 'string' in msg else msg + + return ''.join(map(format, message)) + + def finish(self): + errs = self.panel.find_all_results() + if len(errs) == 0: + sublime.status_message('Build finished') + else: + sublime.status_message('Build finished with %d errors' % len(errs)) + + # Borrowed from Sublime’s ExecCommand: https://github.com/twolfson/sublime-files/blob/master/Packages/Default/exec.py + def update_phantoms(self): + stylesheet = ''' + + ''' + + for file, errs in self.errs_by_file.items(): + view = self.window.find_open_file(file) + if view: + + buffer_id = view.buffer_id() + if buffer_id not in self.phantom_sets_by_buffer: + phantom_set = sublime.PhantomSet(view, "exec") + self.phantom_sets_by_buffer[buffer_id] = phantom_set + else: + phantom_set = self.phantom_sets_by_buffer[buffer_id] + + phantoms = [] + + for line, column, text in errs: + pt = view.text_point(line - 1, column - 1) + phantoms.append(sublime.Phantom( + sublime.Region(pt, view.line(pt).b), + ('' + stylesheet + + '
' + + '' + html.escape(text, quote=False) + '' + + '' + chr(0x00D7) + '
' + + ''), + sublime.LAYOUT_BELOW, + on_navigate=self.on_phantom_navigate)) + + phantom_set.update(phantoms) + + def hide_phantoms(self): + for file, errs in self.errs_by_file.items(): + view = self.window.find_open_file(file) + if view: + view.erase_phantoms('elm_make') + + self.errs_by_file = {} + self.phantom_sets_by_buffer = {} + self.show_errors_inline = False + + def on_phantom_navigate(self, url): + self.hide_phantoms() + + def get_setting(self, key, user_key=None): + package_settings = sublime.load_settings('Elm Language Support.sublime-settings') + user_settings = self.window.active_view().settings() + + return user_settings.get(user_key or ('elm_language_support_' + key), package_settings.get(key)) diff --git a/elm_open_in_browser.py b/elm_open_in_browser.py index 41ac5dc..f27eaa0 100644 --- a/elm_open_in_browser.py +++ b/elm_open_in_browser.py @@ -1,17 +1,10 @@ import webbrowser -try: # ST3 - import urllib.parse as urlparse - import urllib.request as urllib +import urllib.parse as urlparse +import urllib.request as urllib - from .elm_plugin import * - from .elm_project import ElmProject -except: # ST2 - import urlparse - import urllib - - from elm_plugin import * - from elm_project import ElmProject +from .elm_plugin import * +from .elm_project import ElmProject class ElmOpenInBrowserCommand(sublime_plugin.TextCommand): diff --git a/elm_project.py b/elm_project.py index 0157127..b61aee1 100644 --- a/elm_project.py +++ b/elm_project.py @@ -1,10 +1,7 @@ import collections import json -try: # ST3 - from .elm_plugin import * -except: # ST2 - from elm_plugin import * +from .elm_plugin import * class ElmProjectCommand(sublime_plugin.TextCommand): diff --git a/elm_show_type.py b/elm_show_type.py deleted file mode 100644 index 5634fab..0000000 --- a/elm_show_type.py +++ /dev/null @@ -1,279 +0,0 @@ -from __future__ import print_function - -import webbrowser -import os, os.path -import subprocess -import json -import re -from difflib import SequenceMatcher - -import sublime, sublime_plugin - -try: # ST3 - from .elm_project import ElmProject -except: # ST2 - from elm_project import ElmProject - -LOOKUPS = {} - -def join_qualified(region, view): - """ - Given a region, expand outward on periods to return a new region defining - the entire word, in the context of Elm syntax. - - For example, when the region encompasses the 'map' part of a larger - 'Dict.map' word, this function will return the entire region encompassing - 'Dict.map'. The same is true if the region is encompassing 'Dict'. - - Recursively expands outward in both directions, correctly returning longer - constructions such as 'Graphics.Input.button' - """ - starting_region = region - prefix = view.substr(region.a - 1) - suffix = view.substr(region.b) - if prefix == '.': - region = region.cover(view.word(region.a - 2)) - if suffix == '.': - region = region.cover(view.word(region.b + 1)) - - if region == starting_region: - return region - else: - return join_qualified(region, view) - -def get_word_under_cursor(view): - sel = view.sel()[0] - region = join_qualified(view.word(sel), view) - return view.substr(region).strip() - -def get_type(view, panel): - """ - Given a view, return the type signature of the word under the cursor, - if found. If no type is found, return an empty string. Write the info - to an output panel. - """ - sel = view.sel()[0] - region = join_qualified(view.word(sel), view) - scope = view.scope_name(region.b) - if scope.find('source.elm') != -1 and scope.find('string') == -1 and scope.find('comment') == -1: - filename = view.file_name() - word = view.substr(region).strip() - sublime.set_timeout_async(lambda: search_and_set_status_message(filename, word, panel, 0), 0) - -def search_and_set_status_message(filename, query, panel, tries): - """ - Given a filename and a query, look up in the in-memory dict of values - pulled from elm oracle to find a match. If a match is found, display - the type signature in the status bar and set it in the output panel. - """ - global LOOKUPS - if len(query) == 0: - return None - if filename not in LOOKUPS.keys(): - if tries >= 10: - return None - else: - # if the filename is not found loaded into memory, it's probably being - # loaded into memory right now. Try 10 more times at 100ms intervals - # and if it still isn't loaded, there's likely a problem we can't fix - # here. - sublime.set_timeout_async(search_and_set_status_message(filename, query, panel, tries + 1), 100) - else: - data = LOOKUPS[filename] - if len(data) > 0: - matches = [item for item in data if item['name'] == query.split('.')[-1]] - if len(matches) == 0: - return None - else: - # sort matches by similarity to query - matches.sort(key=lambda x: SequenceMatcher(None, query, x['fullName']).ratio(), reverse=True) - item = matches[0] - type_signature = item['fullName'] + ' : ' + item['signature'] - sublime.status_message(type_signature) - panel.run_command('erase_view') - # add full name and type annotation - panel_output = '`' + type_signature + '`' + '\n\n' + item['comment'][1:] - # replace backticks with no-width space for syntax highlighting - panel_output = panel_output.replace('`', '\uFEFF') - # add no-width space to beginning and end of code blocks for syntax highlighting - panel_output = re.sub('\n( {4}[\s\S]+?)((?=\n\S)\n|\Z)', '\uFEFF\n\\1\uFEFF\n', panel_output) - # remove first four spaces on each line from code blocks - panel_output = re.sub('\n {4}', '\n', panel_output) - panel.run_command('append', {'characters': panel_output}) - return None - -def get_matching_names(filename, prefix): - """ - Given a file name and a search prefix, return a list of matching - completions from elm oracle. - """ - def skip_chars(full_name): - # Sublime Text seems to have odd behavior on completions. If the full - # name is at the same "path level" as the prefix, then the completion - # will replace the entire entry, otherwise it will only replace after - # the final period separator - full_name_path = full_name.split('.')[:-1] - prefix_path = prefix.split('.')[:-1] - if full_name_path == prefix_path: - return full_name - else: - # get the characters to remove from the completion to avoid duplication - # of paths. If it's 0, then stay at 0, otherwise add a period back - chars_to_skip = len('.'.join(prefix_path)) - if chars_to_skip > 0: - chars_to_skip += 1 - return full_name[chars_to_skip:] - - global LOOKUPS - if filename not in LOOKUPS.keys(): - return None - else: - data = LOOKUPS[filename] - completions = {(v['fullName'] + '\t' + v['signature'], skip_chars(v['fullName'])) - for v in data - if v['fullName'].startswith(prefix) or v['name'].startswith(prefix)} - return [[v[0], v[1]] for v in completions] - -def explore_package(filename, package_name): - global LOOKUPS - if filename not in LOOKUPS.keys() or len(package_name) == 0: - return None - elif package_name[0].upper() != package_name[0]: - sublime.status_message('This is not a package!') - return None - else: - def open_link(items, i): - if i == -1: - return None - else: - open_in_browser(items[i][3]) - data = [[v['fullName'], v['signature'], v['comment'], v['href']] - for v in LOOKUPS[filename] - if v['fullName'].startswith(package_name)] - # all items must be the same number of rows - n = 75 - panel_items = [v[:2] + [v[2][:n]] + [v[2][n:2*n]] + [v[2][2*n:]] for v in data] - sublime.active_window().show_quick_panel(panel_items, lambda i: open_link(data, i)) - -def open_in_browser(url): - webbrowser.open_new_tab(url) - -def load_from_oracle(filename): - """ - Loads all data about the current file from elm oracle and adds it - to the LOOKUPS global dictionary. - """ - global LOOKUPS - project = ElmProject(filename) - if project.working_dir is None: - return - os.chdir(project.working_dir) - - # Hide the console window on Windows - shell = False - path_separator = ':' - if os.name == "nt": - shell = True - path_separator = ';' - - settings = sublime.load_settings('Elm Language Support.sublime-settings') - path = settings.get('elm_paths', '') - if path: - old_path = os.environ['PATH'] - os.environ["PATH"] = os.path.expandvars(path + path_separator + '$PATH') - - p = subprocess.Popen(['elm-oracle', filename, ''], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, shell=shell) - - if path: - os.environ['PATH'] = old_path - - output, errors = p.communicate() - output = output.strip() - if settings.get('debug', False): - string_settings = sublime.load_settings('Elm User Strings.sublime-settings') - print(string_settings.get('logging.prefix', '') + '(elm-oracle) ' + str(output), '\nerrors: ' + str(errors.strip())) - if str(errors.strip()): - print('Your PATH is: ', os.environ['PATH']) - try: - data = json.loads(output.decode('utf-8')) - except ValueError: - return None - LOOKUPS[filename] = data - -def view_load(view): - """ - Selectively calls load_from_oracle based on the current scope. - """ - - if view.file_name() is None: - return; - - sel = view.sel()[0] - region = join_qualified(view.word(sel), view) - scope = view.scope_name(region.b) - if scope.find('source.elm') != -1: - load_from_oracle(view.file_name()) - - -class ElmOracleListener(sublime_plugin.EventListener): - """ - An event listener to load and search through data from elm oracle. - """ - - def on_selection_modified_async(self, view): - sel = view.sel()[0] - region = join_qualified(view.word(sel), view) - scope = view.scope_name(region.b) - if scope.find('source.elm') != -1: - view.run_command('elm_show_type') - - def on_activated_async(self, view): - view_load(view) - - def on_post_save_async(self, view): - view_load(view) - - def on_query_completions(self, view, prefix, locations): - word = get_word_under_cursor(view) - return get_matching_names(view.file_name(), word) - - -class ElmShowType(sublime_plugin.TextCommand): - """ - A text command to lookup the type signature of the function under the - cursor, and display it in the status bar if found. - """ - type_panel = None - - def run(self, edit, panel=False): - if self.type_panel is None: - self.type_panel = self.view.window().create_output_panel('elm_type') - self.type_panel.set_syntax_file('Packages/Elm Language Support/Syntaxes/Elm Documentation.sublime-syntax') - get_type(self.view, self.type_panel) - if panel: - self.view.window().run_command('elm_show_type_panel') - - -class ElmShowTypePanel(sublime_plugin.WindowCommand): - """ - Turns on the type output panel - """ - def run(self): - self.window.run_command("show_panel", {"panel": "output.elm_type"}) - - -class ElmOracleExplore(sublime_plugin.TextCommand): - def run(self, edit): - word = get_word_under_cursor(self.view) - parts = [part for part in word.split('.') if part[0].upper() == part[0]] - package_name = '.'.join(parts) - explore_package(self.view.file_name(), package_name) - - -class EraseView(sublime_plugin.TextCommand): - """ - Erases a view - """ - def run(self, edit): - self.view.erase(edit, sublime.Region(0, self.view.size())) diff --git a/images/completions.png b/images/completions.png deleted file mode 100644 index b519ce2..0000000 Binary files a/images/completions.png and /dev/null differ diff --git a/images/elm_types.png b/images/elm_types.png deleted file mode 100644 index a17681b..0000000 Binary files a/images/elm_types.png and /dev/null differ diff --git a/images/type_panel.png b/images/type_panel.png deleted file mode 100644 index 8b96566..0000000 Binary files a/images/type_panel.png and /dev/null differ