diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 133306d9..c2302a4c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,6 @@ jobs: - uses: actions/checkout@v2 - uses: SublimeText/syntax-test-action@v2 with: - build: 4143 - default_packages: v4143 + build: 4180 + default_packages: v4180 package_name: ElixirSyntax diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd70c51..90fbccad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [v4.0.0] – 2024-09-01 + +- Elixir: improved matching of right-arrow clauses. +- Elixir: recognize SQL strings inside `query("...")`, `query(Repo, "...")`, `query_many("...")`, `query_many(Repo, "...")` (including bang versions). +- Elixir: fixed expressions in struct headers, e.g.: `%^module{}` and `%@module{}`. +- Elixir: recognize all variants of atom word strings, e.g.: `~w"one two three"a` +- Elixir: fixes to capture expressions: `& 1` is a capture with an integer, not the capture argument `&1`. `& &1.func/2`, `&var.member.func/3` and `&@module.func/1` are captured remote functions. +- HEEx: recognize special attributes `:let`, `:for` and `:if`. +- HEEx: fixed matching dynamic attributes, e.g.: `
`. +- Commands: `mix_test` is better at finding the root `mix.exs` file and runs when the project hasn't been built yet. +- Commands: `mix test` and `mix format` error locations can be double-clicked and jumped to. +- Commands: read `mix` output unbuffered for immediate display in the output panel. +- Commands: removed the `output_scroll_time` setting. The output will scroll automatically without delay. +- Commands: run `mix test` with selected lines if no standard `test` blocks were found, allowing to run tests defined by macros such as `property/2`. +- Commands: prevent executing `mix test` again if it's already running. +- Completions: use double quotes instead of curly braces for `phx` attributes. + ## [v3.2.3] – 2023-08-13 - EEx, HEEx: use `<%!-- ... --%>` when toggling comments. diff --git a/README.md b/README.md index 8e8c4ffd..2250fabb 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ General settings example (via `Preferences > Package Settings > ElixirSyntax > S "mix_test": { "output": "tab", "output_mode": null, - "output_scroll_time": 2, "args": ["--coverage"], "seed": null } diff --git a/color-schemes/Mariana.sublime-color-scheme b/color-schemes/Mariana.sublime-color-scheme index 375735d4..d959da27 100644 --- a/color-schemes/Mariana.sublime-color-scheme +++ b/color-schemes/Mariana.sublime-color-scheme @@ -61,7 +61,8 @@ { "name": "Capture name", "scope": "variable.other.capture.elixir", - "foreground": "color(var(blue))" + "foreground": "color(var(blue))", + "font_style": "italic" }, { "name": "Capture arity", diff --git a/color-schemes/Monokai.sublime-color-scheme b/color-schemes/Monokai.sublime-color-scheme index 3a0a0ca3..cc91d13e 100644 --- a/color-schemes/Monokai.sublime-color-scheme +++ b/color-schemes/Monokai.sublime-color-scheme @@ -2,6 +2,7 @@ "name": "Monokai for Elixir", "variables": { + "white3": "#ddd", "entity": "var(yellow2)", "doc": "var(yellow5)" }, @@ -68,7 +69,8 @@ { "name": "Capture name", "scope": "variable.other.capture.elixir", - "foreground": "color(var(blue))" + "foreground": "color(var(blue))", + "font_style": "italic" }, { "name": "Capture arity", diff --git a/commands/mix_format.py b/commands/mix_format.py index 2ae6a07d..1251ae82 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -70,7 +70,7 @@ def call_mix_format(window, **kwargs): file_path = kwargs.get('file_path') file_path_list = file_path and [file_path] or [] _, cmd_setting = load_mix_format_settings() - cmd = (cmd_setting.get('cmd') or ['mix', 'format']) + file_path_list + cmd_args = (cmd_setting.get('cmd') or ['mix', 'format']) + file_path_list paths = file_path_list + window.folders() cwd = next((reverse_find_root_folder(p) for p in paths if p), None) @@ -79,33 +79,32 @@ def call_mix_format(window, **kwargs): print_status_msg(COULDNT_FIND_MIX_EXS) return - proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0) panel_name = 'mix_format' panel_params = {'panel': 'output.%s' % panel_name} window.run_command('erase_view', panel_params) output_view = None + failed_msg_region = None - past_timestamp = now() - panel_update_interval = 2 - - while proc.poll() is None: - line = proc.stdout.readline().decode(encoding='UTF-8') - - if line: + try: + for text in read_proc_text_output(proc): if not output_view: - output_view = create_mix_format_panel(window, panel_name, cmd, cwd) + # Only open the panel when mix is compiling or there is an error. + output_view = create_mix_format_panel(window, panel_name, cmd_args, cwd) window.run_command('show_panel', panel_params) - output_view.run_command('append', {'characters': line}) - - if now() - past_timestamp > panel_update_interval: - output_view.show(output_view.size()) - past_timestamp = now() + output_view.run_command('append', {'characters': text, 'disable_tab_translation': True}) + except BaseException as e: + write_output(PRINT_PREFIX + " Exception: %s" % repr(e)) if output_view: output_view.set_read_only(True) - else: + failed_msg_region = output_view.find("mix format failed", 0, sublime.IGNORECASE) + failed_msg_region and output_view.show_at_center(failed_msg_region) + + # Either there was no output or there was but without an error. + if not output_view or not failed_msg_region: if window.active_panel() == panel_params['panel']: window.run_command('hide_panel', panel_params) window.destroy_output_panel(panel_name) @@ -118,8 +117,10 @@ def create_mix_format_panel(window, panel_name, cmd_args, cwd): first_lines += '\n# Timestamp: %s\n\n' % datetime.now().replace(microsecond=0) output_view = window.create_output_panel(panel_name) - output_view.settings().set('result_file_regex', r'/([^/]+):(\d+):(\d+)') + output_view.settings().set('result_file_regex', MIX_RESULT_FILE_REGEX) + output_view.settings().set('result_base_dir', cwd) output_view.set_read_only(False) output_view.run_command('append', {'characters': first_lines}) + output_view.run_command('move_to', {'to': 'eof'}) return output_view diff --git a/commands/mix_test.py b/commands/mix_test.py index baffec29..c694491a 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -132,7 +132,14 @@ def contains_all_tests(describe_region): for header_and_name_regions, describe_tuple in selected_test_regions ] - params = {'abs_file_path': abs_file_path, 'names': selected_tests} + # Use the selected lines if no tests were found. + if selected_tests: + params = {'names': selected_tests} + else: + params = {'lines': list(self.view.rowcol(min(sel.a, sel.b))[0] + 1 for sel in self.view.sel())} + + params.setdefault('abs_file_path', abs_file_path) + call_mix_test_with_settings(self.view.window(), **params) # This function is unused but kept to have a fallback in case @@ -444,37 +451,44 @@ def reverse_find_json_path(window, json_file_path): paths = [window.active_view().file_name()] + window.folders() root_dir = next((reverse_find_root_folder(p) for p in paths if p), None) - root_dir or print_status_msg(COULDNT_FIND_MIX_EXS) + if not root_dir: + sublime.message_dialog(COULDNT_FIND_MIX_EXS) + print_status_msg(COULDNT_FIND_MIX_EXS) return root_dir and path.join(root_dir, json_file_path) or None def call_mix_test_with_settings(window, **params): """ Calls `mix test` with the settings JSON merged with the given params. """ + try_run_mix_test(window, params) + +def merge_mix_settings_and_params(window, params): + """ Merges the settings JSON with the given params. """ mix_settings_path = reverse_find_json_path(window, FILE_NAMES.SETTINGS_JSON) if not mix_settings_path: return root_dir = path.dirname(mix_settings_path) - build_dir = path.join(root_dir, '_build') if 'abs_file_path' in params: params.setdefault('file_path', path.relpath(params['abs_file_path'], root_dir)) del params['abs_file_path'] - save_json_file(path.join(build_dir, FILE_NAMES.REPEAT_JSON), params) + build_dir = Path(root_dir) / '_build' + build_dir.exists() or build_dir.mkdir() + save_json_file(str(build_dir / FILE_NAMES.REPEAT_JSON), params) mix_params = load_json_file(mix_settings_path) mix_params = remove_help_info(mix_params) mix_params.update(params) mix_params.setdefault('cwd', root_dir) - call_mix_test(window, mix_params, root_dir) + return (mix_params, root_dir) -def call_mix_test(window, mix_params, cwd): +def get_mix_test_arguments(window, mix_params, cwd): """ Calls `mix test` in an asynchronous thread. """ - cmd, file_path, names, seed, failed, args = \ - list(map(mix_params.get, ('cmd', 'file_path', 'names', 'seed', 'failed', 'args'))) + cmd, file_path, names, lines, seed, failed, args = \ + list(map(mix_params.get, ('cmd', 'file_path', 'names', 'lines', 'seed', 'failed', 'args'))) located_tests, unlocated_tests = \ names and find_lines_using_test_names(path.join(cwd, file_path), names) or (None, None) @@ -485,6 +499,9 @@ def call_mix_test(window, mix_params, cwd): if file_path and located_tests: file_path += ''.join(':%s' % l for (_t, _n, l) in located_tests) + if file_path and lines: + file_path += ''.join(':%s' % l for l in lines) + mix_test_pckg_settings = sublime.load_settings(SETTINGS_FILE_NAME).get('mix_test', {}) def get_setting(key): @@ -499,17 +516,35 @@ def get_setting(key): cmd_arg = cmd or ['mix', 'test'] failed_arg = failed and ['--failed'] or [] mix_command = cmd_arg + seed_arg + file_path_arg + (args or []) + failed_arg - print(PRINT_PREFIX, '`%s` parameters:' % ' '.join(cmd_arg), mix_params) - sublime.set_timeout_async( - lambda: write_to_output(window, mix_command, mix_params, cwd, get_setting) - ) + return (cmd_arg, mix_command, get_setting) + +IS_MIX_TEST_RUNNING = False + +def try_run_mix_test_async(window, params): + global IS_MIX_TEST_RUNNING + + try: + IS_MIX_TEST_RUNNING = True + (mix_params, cwd) = merge_mix_settings_and_params(window, params) + (cmd_arg, mix_command, get_setting) = get_mix_test_arguments(window, mix_params, cwd) + print('%s `%s` parameters: %s' % (PRINT_PREFIX, ' '.join(cmd_arg), repr(mix_params))) + run_mix_test(window, mix_command, mix_params, cwd, get_setting) + finally: + IS_MIX_TEST_RUNNING = False -def write_to_output(window, cmd_args, params, cwd, get_setting): +def try_run_mix_test(window, params): + if IS_MIX_TEST_RUNNING: + # NB: showing a blocking dialog here stops the reading of the subprocess output somehow. + sublime.set_timeout_async(lambda: sublime.message_dialog('The `mix test` process is still running!')) + print_status_msg('mix test is already running!') + return + + sublime.set_timeout_async(lambda: try_run_mix_test_async(window, params)) + +def run_mix_test(window, cmd_args, params, cwd, get_setting): """ Creates the output view/file and runs the `mix test` process. """ mix_test_output = get_setting('output') or 'panel' - output_scroll_time = get_setting('output_scroll_time') - output_scroll_time = output_scroll_time if type(output_scroll_time) == int else None output_view = output_file = None if type(mix_test_output) != str: @@ -548,14 +583,14 @@ def write_to_output(window, cmd_args, params, cwd, get_setting): output_view.set_name('mix test' + (file_path and ' ' + file_path or '')) ov_settings = output_view.settings() ov_settings.set('word_wrap', active_view_settings.get('word_wrap')) - ov_settings.set('result_file_regex', r'^\s+(.+?):(\d+)$') - # ov_settings.set('result_line_regex', r'^:(\d+)') + ov_settings.set('result_file_regex', MIX_RESULT_FILE_REGEX) ov_settings.set('result_base_dir', cwd) output_view.set_read_only(False) def write_output(txt): if output_file: output_file.write(txt) + output_file.flush() else: output_view.run_command('append', {'characters': txt, 'disable_tab_translation': True}) @@ -574,13 +609,14 @@ def write_output(txt): ) return - proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0) if output_view: output_view.settings().set('view_id', output_view.id()) cmd = ' '.join(params.get('cmd') or ['mix test']) - first_lines = '$ cd %s && %s' % (shlex.quote(cwd), ' '.join(map(shlex.quote, cmd_args))) + cmd_string = ' '.join(map(shlex.quote, cmd_args)) + first_lines = '$ cd %s && %s' % (shlex.quote(cwd), cmd_string) first_lines += '\n# `%s` pid: %s' % (cmd, proc.pid) first_lines += '\n# Timestamp: %s' % datetime.now().replace(microsecond=0) if params.get('names'): @@ -591,43 +627,41 @@ def write_output(txt): print(PRINT_PREFIX + ''.join('\n' + (line and ' ' + line) for line in first_lines.split('\n'))) write_output(first_lines + '\n\n') + output_view and output_view.run_command('move_to', {'to': 'eof'}) - past_time = now() + continue_hidden = False - while proc.poll() is None: - if output_file and fstat(output_file.fileno()).st_nlink == 0 \ - or output_view and not output_view.window(): - on_output_close(proc, cmd) - break - - try: - write_output(proc.stdout.readline().decode(encoding='UTF-8')) + try: + for text in read_proc_text_output(proc): + if not continue_hidden \ + and (output_file and fstat(output_file.fileno()).st_nlink == 0 \ + or output_view and not output_view.window()): + continue_hidden = continue_on_output_close(proc, cmd) + if not continue_hidden: + break - if output_scroll_time != None and now() - past_time > output_scroll_time: - if output_file: - output_file.flush() - else: - output_view.show(output_view.size()) - past_time = now() - except: - break + write_output(text) + except BaseException as e: + write_output(PRINT_PREFIX + "Exception: %s" % repr(e)) if output_file: output_file.close() else: output_view.set_read_only(True) - output_scroll_time != None and output_view.show(output_view.size()) -def on_output_close(proc, cmd): - if proc.poll() is None: - can_stop = sublime.ok_cancel_dialog( - 'The `%s` process is still running. Stop the process?' % cmd, - ok_title='Yes', title='Stop running `%s`' % cmd - ) + print_status_msg('Finished `%s`!' % cmd_string) + +def continue_on_output_close(proc, cmd): + can_continue = sublime.ok_cancel_dialog( + 'The `%s` process is still running. Continue in the background?' % cmd, + ok_title='Yes', title='Continue running `%s`' % cmd + ) + + if not can_continue: + print_status_msg('Stopping `%s` (pid=%s).' % (cmd, proc.pid)) + proc.send_signal(subprocess.signal.SIGQUIT) - if can_stop: - print_status_msg('Stopping `%s` (pid=%s).' % (cmd, proc.pid)) - proc.send_signal(subprocess.signal.SIGQUIT) + return can_continue def add_help_info(dict_data): dict_data['help'] = { @@ -636,7 +670,6 @@ def add_help_info(dict_data): 'output_mode': {'description': 'Output mode of the disk file to open/create.', 'default': 'w', 'values': 'see `open()` modifiers'}, 'cmd': {'description': 'Which command to execute.', 'default': ['mix', 'test']}, 'args': {'description': 'Additional arguments to pass to `cmd`.', 'default': [], 'values': 'see `mix help test`'}, - 'output_scroll_time': {'description': 'Automatically scroll the output view every t seconds. `null` disables scrolling.', 'default': 2, 'values': [None, 'non-negative float']}, 'seed': {'description': 'The seed with which to randomize the tests.', 'default': None, 'values': [None, 'non-negative integer']}, } return dict_data diff --git a/commands/utils.py b/commands/utils.py index 6606e3a5..c9075123 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -10,9 +10,12 @@ PRINT_PREFIX = 'ElixirSyntax:' +# The regex is used by Sublime to find and jump to error locations shown in output panels. +MIX_RESULT_FILE_REGEX = r'(\S+?[/\\]\S+?\.[a-zA-Z]+):(\d+)(?::(\d+))?' + COULDNT_FIND_MIX_EXS = \ - 'Error: could not find a mix.exs file and the _build/ directory!\n' + \ - 'Make sure that you are in a mix project and that `mix \'do\' deps.get, compile` has been run.' + 'Error: could not find a mix.exs file!\n' + \ + 'Make sure that you are in a mix project.' def print_status_msg(msg): print(PRINT_PREFIX, msg) @@ -50,14 +53,31 @@ def reverse_find_root_folder(bottom_path): parent_path = bottom_path.parent if bottom_path.is_file() else bottom_path while True: - if all((parent_path / p).exists() for p in ['mix.exs', '_build']): + # We have to check for the root mix.exs, ignoring possible sub-app mix files. + if (parent_path / 'mix.exs').exists() \ + and ( + (parent_path / 'mix.lock').exists() + or (parent_path / '_build').exists() + or parent_path.name != 'apps' and not (parent_path.parent / 'mix.exs').exists() + ): return str(parent_path) + old_path, parent_path = parent_path, parent_path.parent + if old_path == parent_path: break return None +def read_proc_text_output(proc, size=1024): + while proc.poll() is None: + # TODO: the subprocess should be opened with an encoding to avoid the decode call, + # but the option is not supported in Sublime's Python yet. + text = proc.stdout.read(size).decode(encoding='UTF-8') + if not text: continue + yield text + return '' + def save_json_file(file_path, dict_data): try: with open(str(file_path), 'w') as file: diff --git a/completions/Phoenix_Attributes.sublime-completions b/completions/Phoenix_Attributes.sublime-completions index 21b1b86e..580b36a3 100644 --- a/completions/Phoenix_Attributes.sublime-completions +++ b/completions/Phoenix_Attributes.sublime-completions @@ -3,145 +3,145 @@ "completions": [ { "trigger": "phx-value-*", - "contents": "phx-value-${1:*}={$2}", + "contents": "phx-value-${1:*}=\"$2\"", "kind": "snippet", "details": "Params" }, { "trigger": "phx-click", - "contents": "phx-click={$1}", + "contents": "phx-click=\"$1\"", "kind": "snippet", "details": "Click Events" }, { "trigger": "phx-click-away", - "contents": "phx-click-away={$1}", + "contents": "phx-click-away=\"$1\"", "kind": "snippet", "details": "Click Events" }, { "trigger": "phx-change", - "contents": "phx-change={$1}", + "contents": "phx-change=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-submit", - "contents": "phx-submit={$1}", + "contents": "phx-submit=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-feedback-for", - "contents": "phx-feedback-for={$1}", + "contents": "phx-feedback-for=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-disable-with", - "contents": "phx-disable-with={$1}", + "contents": "phx-disable-with=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-trigger-action", - "contents": "phx-trigger-action={$1}", + "contents": "phx-trigger-action=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-auto-recover", - "contents": "phx-auto-recover={$1}", + "contents": "phx-auto-recover=\"$1\"", "kind": "snippet", "details": "Form Events" }, { "trigger": "phx-blur", - "contents": "phx-blur={$1}", + "contents": "phx-blur=\"$1\"", "kind": "snippet", "details": "Focus Events" }, { "trigger": "phx-focus", - "contents": "phx-focus={$1}", + "contents": "phx-focus=\"$1\"", "kind": "snippet", "details": "Focus Events" }, { "trigger": "phx-window-blur", - "contents": "phx-window-blur={$1}", + "contents": "phx-window-blur=\"$1\"", "kind": "snippet", "details": "Focus Events" }, { "trigger": "phx-window-focus", - "contents": "phx-window-focus={$1}", + "contents": "phx-window-focus=\"$1\"", "kind": "snippet", "details": "Focus Events" }, { "trigger": "phx-keydown", - "contents": "phx-keydown={$1}", + "contents": "phx-keydown=\"$1\"", "kind": "snippet", "details": "Key Events" }, { "trigger": "phx-keyup", - "contents": "phx-keyup={$1}", + "contents": "phx-keyup=\"$1\"", "kind": "snippet", "details": "Key Events" }, { "trigger": "phx-window-keydown", - "contents": "phx-window-keydown={$1}", + "contents": "phx-window-keydown=\"$1\"", "kind": "snippet", "details": "Key Events" }, { "trigger": "phx-window-keyup", - "contents": "phx-window-keyup={$1}", + "contents": "phx-window-keyup=\"$1\"", "kind": "snippet", "details": "Key Events" }, { "trigger": "phx-key", - "contents": "phx-key={$1}", + "contents": "phx-key=\"$1\"", "kind": "snippet", "details": "Key Events" }, { "trigger": "phx-update", - "contents": "phx-update={$1}", + "contents": "phx-update=\"$1\"", "kind": "snippet", "details": "DOM Patching" }, { "trigger": "phx-remove", - "contents": "phx-remove={$1}", + "contents": "phx-remove=\"$1\"", "kind": "snippet", "details": "DOM Patching" }, { "trigger": "phx-hook", - "contents": "phx-hook={$1}", + "contents": "phx-hook=\"$1\"", "kind": "snippet", "details": "JS Interop" }, { "trigger": "phx-debounce", - "contents": "phx-debounce={$1}", + "contents": "phx-debounce=\"$1\"", "kind": "snippet", "details": "Rate Limiting" }, { "trigger": "phx-throttle", - "contents": "phx-throttle={$1}", + "contents": "phx-throttle=\"$1\"", "kind": "snippet", "details": "Rate Limiting" }, { "trigger": "phx-track-static", - "contents": "phx-track-static={$1}", + "contents": "phx-track-static=\"$1\"", "kind": "snippet", "details": "Static Tracking" } diff --git a/settings/ElixirSyntax.sublime-settings b/settings/ElixirSyntax.sublime-settings index 9b1586bc..dab8ae70 100644 --- a/settings/ElixirSyntax.sublime-settings +++ b/settings/ElixirSyntax.sublime-settings @@ -8,9 +8,6 @@ // Output mode of the disk file to open/create. // `{"values": "see `open()` modifiers"}` "output_mode": "w", - // Automatically scroll the output view every t seconds. `null` disables scrolling. - // `{"values": [null, "non-negative float"]}` - "output_scroll_time": 2, // Additional arguments to pass to `cmd`. // `{"values": "see `mix help test`"}` "args": [], diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 4b89c5ed..de9713f9 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -120,47 +120,45 @@ contexts: fn_block: - match: fn{{no_id_key_suffix}} scope: punctuation.section.block.begin.elixir keyword.other.fn.elixir - push: [fn_block_end_pop, arrow_clauses_body_pop, fn_single_body_or_pop] - - fn_single_body_or_pop: - - match: (?=->) - set: - - match: (?=end{{no_id_key_suffix}}) - pop: 1 - - include: core_syntax - - include: if_non_space_or_eol_pop + push: [fn_block_end_pop, arrow_clauses_body_pop] fn_block_end_pop: - include: block_end_pop - - include: if_closing_token_pop - - include: core_syntax + - include: core_syntax_or_if_closing_pop arrow_clauses_body_pop: + - include: if_closing_token_pop - match: (?=\S) - set: - - match: (?=->|when{{no_id_key_suffix}}) - push: inlined_core_syntax_pop - # NB: no default parameters in arrow clauses - - match: \\\\(?!:{{no_colon_suffix}}) - scope: keyword.operator.default.elixir invalid.illegal.default-operator.elixir - - include: parameters_or_if_closing_pop - - match: $ - set: - - include: if_closing_token_pop - - match: ^(\s*)(?=[^#\s]) - push: [indented_core_syntax_pop, params_until_arrow_pop] - - include: core_syntax + branch_point: non_arrow_clause_branch + branch: [non_arrow_core_syntax_pop, params_until_arrow_pop] - inlined_core_syntax_pop: - - match: (?=;) + non_arrow_core_syntax_pop: + - match: (?=->(?!:)|when{{no_id_key_suffix}}) + fail: non_arrow_clause_branch + - match: (?!@)(?!&(?!&))(?={{operator}}(?!:)) + push: + - include: operator + - include: if_non_space_pop + - match: (?=;|$) pop: 1 - include: core_syntax_or_if_closing_pop params_until_arrow_pop: - - match: (?=->|when{{no_id_key_suffix}}) - pop: 1 + - match: (?=when{{no_id_key_suffix}}) + set: + - include: arrow_operator_pop + - include: core_syntax_or_if_closing_pop + - include: arrow_operator_pop + - match: \\\\(?!:{{no_colon_suffix}}) + comment: no default parameters in arrow clauses + scope: keyword.operator.default.elixir invalid.illegal.default-operator.elixir - include: parameters_or_if_closing_pop + arrow_operator_pop: + - match: ->(?!:) + scope: keyword.operator.arrow.elixir + pop: 1 + indented_core_syntax_pop: - match: ^(?=\1[^#\s]|(?!\1)(?!\s*(#|$))) pop: 1 @@ -314,7 +312,7 @@ contexts: parameters: - include: block_or_keyword - include: special_form - - match: (?=\^\s*{{identifier}}{{no_key_suffix}}(?!\s*\.(?!\.))(?!{{has_arguments}})) + - match: (?x)(?=\^ \s* {{identifier}}{{no_key_suffix}} (?!\s*\.(?!\.)) (?!{{has_arguments}})) push: - include: operator - include: identifier_pop @@ -345,7 +343,7 @@ contexts: - include: parameters id_or_operator_call_param_pop: - - match: ({{member}}){{no_key_suffix}} + - match: ({{member}}){{no_suffix_then_arguments}} scope: variable.function.elixir set: - include: arguments_paren_param_pop @@ -375,7 +373,7 @@ contexts: - match: (?={{module_name}}\s*\.(?!\.)) scope: constant.other.module.elixir push: module_function_call_param_pop - - match: (?={{identifier}}{{no_key_suffix}}(?=\s*\.\s*\(|{{has_arguments}})) + - match: (?={{identifier}}{{no_key_suffix}}\s*\.?\s*\() push: id_or_operator_call_param_pop paren_param: @@ -430,8 +428,8 @@ contexts: - meta_scope: meta.mapping.elixir - match: (?=unquote\() set: [map_param_unquote_meta_pop, arguments_pop, unquote_pop] - - include: alias_names - - match: ((?=_)(?){{identifier}})|({{identifier}}) + - include: special_form + - match: (?>((?=_)(?){{identifier}})|({{identifier}}))(?![(.]) captures: 1: variable.parameter.unused.elixir 2: variable.parameter.elixir @@ -444,15 +442,20 @@ contexts: - include: if_non_space_pop map_param_body_pop: - - match: (?={) + - match: \{ + scope: punctuation.section.mapping.begin.elixir set: - - match: \{ - scope: punctuation.section.mapping.begin.elixir - set: - - meta_scope: meta.mapping.elixir - - include: map_closing_pop - - include: parameters_or_if_closing_pop - - include: if_closing_token_pop + - meta_scope: meta.mapping.elixir + - include: map_closing_pop + - include: parameters_or_if_closing_pop + - match: (?=\^) + push: + - match: (?={) + pop: true + - include: special_form + - include: modules_or_ids_or_paren_calls + - include: core_syntax_or_if_closing_pop + - include: parameters_or_if_closing_pop - include: if_non_space_or_eol_pop binary_string_param: @@ -983,29 +986,6 @@ contexts: captures: 1: invalid.illegal.sigil.elixir - # Look for 'a' behind the closing delimiter. - # Bracket delimiters are not matched yet: <>, {}, [] and () - - match: (?=~w([/|"'])(?>\\.|(?!\1).)*\1a) - comment: highlight words as atoms - push: - - match: (~w)(.) - captures: - 1: storage.type.string.elixir - 2: string.quoted.other.atom.elixir punctuation.definition.string.begin.elixir - set: - - meta_scope: meta.string.elixir - - meta_content_scope: string.quoted.other.atom.elixir constant.other.symbol.atom.elixir - - match: \s+ - push: - - clear_scopes: 1 - - include: if_empty_pop - - include: escaped_or_interpolated - - match: (\2)(a) - captures: - 1: string.quoted.other.atom.elixir punctuation.definition.string.end.elixir - 2: string.quoted.modifiers.elixir storage.type.string.elixir - pop: 1 - - match: ~L(?=["'])(?!''') comment: LiveView scope: meta.string.elixir storage.type.string.elixir @@ -1426,54 +1406,16 @@ contexts: scope: constant.character.escape.pcree - include: if_closing_paren_pop + - match: ~w + comment: with atom words and with interpolation + scope: meta.string.elixir storage.type.string.elixir + branch_point: word_atoms_sigil + branch: [word_atoms_sigil_with_interpolation_pop, sigil_with_interpolation_pop] + - match: ~[a-z] comment: with sigil and with interpolation scope: meta.string.elixir storage.type.string.elixir - push: - - match: (?="""|''') - set: - - match: (?<="""|''') - set: string_modifiers_and_pop - - include: heredoc_string_interpolated - - match: (?=[/|"']) - set: - - meta_scope: meta.string.elixir - # (?<=[a-z]) avoids matching again after the closing delimiter. E.g.: ~s||// - - match: (?<=[a-z])([/|"']) - captures: - 1: string.quoted.other.literal.lower.elixir punctuation.definition.string.begin.elixir - push: - - meta_content_scope: string.quoted.other.literal.lower.elixir - - match: \1 - scope: string.quoted.other.literal.lower.elixir punctuation.definition.string.end.elixir - pop: 1 - - include: escaped_or_interpolated - - match: '' - set: string_modifiers_and_pop - - match: \{ - scope: punctuation.definition.string.begin.elixir - set: - - meta_scope: meta.string.elixir string.interpolated.elixir - - include: string_closing_curly - - include: escaped_or_interpolated - - match: \[ - scope: punctuation.definition.string.begin.elixir - set: - - meta_scope: meta.string.elixir string.interpolated.elixir - - include: string_closing_square - - include: escaped_or_interpolated - - match: \< - scope: punctuation.definition.string.begin.elixir - set: - - meta_scope: meta.string.elixir string.interpolated.elixir - - include: string_closing_angle - - include: escaped_or_interpolated - - match: \( - scope: punctuation.definition.string.begin.elixir - set: - - meta_scope: meta.string.elixir string.interpolated.elixir - - include: string_closing_round - - include: escaped_or_interpolated + push: sigil_with_interpolation_pop - match: ~[A-Z]+ comment: with sigil and without interpolation @@ -1545,6 +1487,138 @@ contexts: - meta_scope: meta.string.elixir string.quoted.other.literal.upper.elixir - include: string_closing_round + string_atom_char: + - match: \S + scope: string.quoted.other.atom.elixir constant.other.symbol.atom.elixir + + word_atoms_sigil_with_interpolation_pop: + - match: (?="""|''') + set: + - match: (?<="""|''')(?![a-zA-Z\d]*a) + fail: word_atoms_sigil + - match: (?<="""|''') + set: string_modifiers_and_pop + - match: (""")(.*)\n + comment: Triple-quoted heredocs + captures: + 1: punctuation.definition.string.begin.elixir + 2: invalid.illegal.opening-heredoc.elixir + push: + - meta_scope: meta.string.elixir string.quoted.triple.double.elixir + - include: escaped_or_interpolated + - include: heredoc_string_closing_double_pop + - include: string_atom_char + - match: (''')(.*)\n + comment: Triple-quoted heredocs + captures: + 1: punctuation.definition.string.begin.elixir + 2: invalid.illegal.opening-heredoc.elixir + push: + - meta_scope: meta.string.elixir string.quoted.triple.single.elixir + - include: escaped_or_interpolated + - include: heredoc_string_closing_single_pop + - include: string_atom_char + - match: (?=[/|"']) + set: + - meta_scope: meta.string.elixir + # (?<=w) avoids matching again after the closing delimiter. E.g.: ~s||// + - match: (?<=w)([/|"']) + captures: + 1: string.quoted.other.literal.lower.elixir punctuation.definition.string.begin.elixir + push: + - meta_content_scope: string.quoted.other.literal.lower.elixir + - match: \1 + scope: string.quoted.other.literal.lower.elixir punctuation.definition.string.end.elixir + pop: 1 + - include: escaped_or_interpolated + - include: string_atom_char + - match: (?![a-zA-Z\d]*a) + fail: word_atoms_sigil + - match: '' + set: string_modifiers_and_pop + - match: \{ + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - match: (?=\}(?![a-zA-Z\d]*a)) + fail: word_atoms_sigil + - include: string_closing_curly + - include: escaped_or_interpolated + - include: string_atom_char + - match: \[ + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - match: (?=\](?![a-zA-Z\d]*a)) + fail: word_atoms_sigil + - include: string_closing_square + - include: escaped_or_interpolated + - include: string_atom_char + - match: \< + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - match: (?=\>(?![a-zA-Z\d]*a)) + fail: word_atoms_sigil + - include: string_closing_angle + - include: escaped_or_interpolated + - include: string_atom_char + - match: \( + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - match: (?=\)(?![a-zA-Z\d]*a)) + fail: word_atoms_sigil + - include: string_closing_round + - include: escaped_or_interpolated + - include: string_atom_char + + sigil_with_interpolation_pop: + - match: (?="""|''') + set: + - match: (?<="""|''') + set: string_modifiers_and_pop + - include: heredoc_string_interpolated + - match: (?=[/|"']) + set: + - meta_scope: meta.string.elixir + # (?<=[a-z]) avoids matching again after the closing delimiter. E.g.: ~s||// + - match: (?<=[a-z])([/|"']) + captures: + 1: string.quoted.other.literal.lower.elixir punctuation.definition.string.begin.elixir + push: + - meta_content_scope: string.quoted.other.literal.lower.elixir + - match: \1 + scope: string.quoted.other.literal.lower.elixir punctuation.definition.string.end.elixir + pop: 1 + - include: escaped_or_interpolated + - match: '' + set: string_modifiers_and_pop + - match: \{ + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - include: string_closing_curly + - include: escaped_or_interpolated + - match: \[ + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - include: string_closing_square + - include: escaped_or_interpolated + - match: \< + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - include: string_closing_angle + - include: escaped_or_interpolated + - match: \( + scope: punctuation.definition.string.begin.elixir + set: + - meta_scope: meta.string.elixir string.interpolated.elixir + - include: string_closing_round + - include: escaped_or_interpolated + string_closing_dquote: - match: \" scope: punctuation.definition.string.end.elixir @@ -1664,9 +1738,10 @@ contexts: - include: unquote_call - include: dot_operator - include: if_non_space_or_eol_pop - - include: arg_comma_and_skip_ws + - match: (?=,) + push: arguments_ws_rest_pop - include: last_id_argument - - include: core_syntax_or_if_closing_pop + - include: core_syntax defrecord: - match: (?:(Record)\s*(\.)\s*)?(defrecordp?){{no_suffix_then_arguments}} @@ -1816,6 +1891,15 @@ contexts: scope: variable.function.elixir set: arguments_paren_or_ws_pop + id_or_operator_paren_call_pop: + - match: ({{member}})(?=\s*\.?\s*\() + scope: variable.function.elixir + set: id_or_operator_paren_call_arguments_pop + + id_or_operator_paren_call_arguments_pop: + - include: dot_operator + - include: arguments_pop + quoted_remote_call_pop: - match: \"(?=(?>\\[\\"]|.)*?"{{has_arguments}}) scope: punctuation.definition.constant.begin.elixir @@ -1897,6 +1981,15 @@ contexts: - include: module_name_pop - include: identifier_pop + modules_or_ids_or_paren_calls: + - include: atom_module_name_call + - match: (?={{module_name}}|{{identifier}}) + push: + - include: unquote_call_pop + - include: id_or_operator_paren_call_pop + - include: module_name_pop + - include: identifier_pop + identifier_pop: - match: ((?=_)(?){{identifier}})|({{identifier}}) captures: @@ -1924,6 +2017,7 @@ contexts: push: member_or_call_pop member_or_call_pop: + - include: query_function_call - include: module_function_call_pop - include: unquote_call_pop - include: id_or_operator_call_pop @@ -2185,6 +2279,11 @@ contexts: - match: (?=unquote\() set: [map_unquote_meta_pop, arguments_pop, unquote_pop] - include: alias_names + - match: (@)({{identifier}}(?=\s*{)) + captures: + 1: keyword.operator.attribute.elixir + 2: variable.other.constant.elixir + set: map_body_pop - match: \^ scope: keyword.operator.pin.elixir - match: ((?=_)(?){{identifier}})|({{identifier}}) @@ -2332,10 +2431,16 @@ contexts: ## Captures capture: - - match: (&)\s*(\d+)(?!\.(?![.\D])) + - include: capture_arg_number + - include: capture_remote_func + + capture_arg_number: + - match: (&)(\d+)(?!\.(?![.\D])) captures: 1: punctuation.definition.capture.elixir constant.other.capture.elixir 2: constant.other.capture.elixir + + capture_remote_func: - match: \& scope: keyword.operator.capture.elixir push: @@ -2343,10 +2448,14 @@ contexts: pop: 1 - include: if_closing_token_pop - match: (?=\.(?!\.)) - set: + push: - include: dot_operator - include: capture_name_pop - include: member_or_call_pop + - match: (?=&\d+\s*\.(?!\.)) + push: + - include: capture_arg_number + - include: if_empty_pop - include: capture_name_pop - include: special_form - match: (?={{identifier}}\s*\.(?!\.|\s*\()) @@ -2354,6 +2463,8 @@ contexts: - include: if_non_space_or_eol_pop capture_name_pop: + - match: (?={{module_name}}\s*\.\s*{{identifier}}\s*+(?!/(?!/))) + set: module_function_call_pop - match: | (?x)(?= (?> @@ -2366,6 +2477,9 @@ contexts: ) comment: exit if module/atom is not followed by `.` or `.(` pop: 1 + - match: '@(?!/)' + scope: keyword.operator.attribute.elixir + push: module_attribute_pop - include: atom_symbol - include: atom_keyword - include: module_name @@ -2388,6 +2502,8 @@ contexts: ## SQL sql_or_fragment: + - include: query_function_call + - match: (?>(fragment)|(sql))(\() captures: 1: support.function.elixir @@ -2399,6 +2515,25 @@ contexts: set: [arguments_rest_pop, sql_unquote_pop, unquote_pop] - include: sql_or_fragment_args_pop + query_function_call: + - match: (?=(?:(?>Repo|SQL)\s*\.\s*)?query(?:_many)?!?\() + push: + - include: module_name + - include: dot_operator + - match: (query(?:_many)?!?)(\() + captures: + 1: variable.function.elixir + 2: punctuation.section.arguments.begin.elixir + set: [sql_or_fragment_args_pop, sql_maybe_skip_non_string_arg_pop] + + sql_maybe_skip_non_string_arg_pop: + - match: \, + scope: punctuation.separator.arguments.elixir + pop: 1 + - match: (?=") + pop: 1 + - include: core_syntax_or_if_closing_pop + sql_unquote_pop: - match: \( scope: punctuation.section.arguments.begin.elixir @@ -2710,6 +2845,10 @@ contexts: - match: (?=(?><-|->|=(?![>~=]))(?!:)) fail: last_assignment_clause - include: if_argument_end_pop + - match: \s*\n + push: + - include: comments + - include: if_non_space_pop - include: arguments_ws_rest_pop case_macro_call: diff --git a/syntaxes/HTML (HEEx).sublime-syntax b/syntaxes/HTML (HEEx).sublime-syntax index 60cafe58..858e7d35 100644 --- a/syntaxes/HTML (HEEx).sublime-syntax +++ b/syntaxes/HTML (HEEx).sublime-syntax @@ -22,6 +22,11 @@ contexts: tag-attributes: - meta_prepend: true - include: heex-phx-attributes + - include: heex-special-attributes + - include: elixir-embedded + - match: = + scope: invalid.attribute.heex punctuation.separator.key-value.html + push: [tag-generic-attribute-meta, tag-generic-attribute-value] tag-other: - meta_prepend: true @@ -106,6 +111,30 @@ contexts: scope: entity.other.attribute-name.heex push: [tag-generic-attribute-meta, tag-generic-attribute-assignment] + heex-special-attributes: + - match: (:)(if|let)(?=={) + captures: + 2: entity.other.attribute-name.heex + push: [tag-generic-attribute-meta, tag-generic-attribute-assignment] + - match: (:)(for)(?=={) + captures: + 2: entity.other.attribute-name.heex + push: [tag-generic-attribute-meta, heex-for-attribute-pop] + + heex-for-attribute-pop: + - match: (=)(\{) + captures: + 1: punctuation.separator.key-value.html + 2: punctuation.section.embedded.begin.elixir + set: + - meta_scope: meta.embedded.heex + - meta_content_scope: source.elixir.embedded.html + - match: \} + scope: punctuation.section.embedded.end.elixir + pop: 1 + - include: scope:source.elixir#one_arrow_clause_or_argument + apply_prototype: true + # Elixir: elixir-embedded: diff --git a/tests/syntax_test_declarations.ex b/tests/syntax_test_declarations.ex index 86b501b0..5bf61ca0 100644 --- a/tests/syntax_test_declarations.ex +++ b/tests/syntax_test_declarations.ex @@ -58,6 +58,18 @@ defmodule :App.:Module # ^ punctuation.accessor # ^^^ constant.other.module # ^ punctuation.definition.constant.begin +defmodule Module, NotAModule +# ^^^^^^^^^^^^^^ -entity.name.namespace +# ^ punctuation.separator.arguments +defmodule(Module, NotAModule) +# ^^^^^^^^^^^^^^ -entity.name.namespace +# ^ punctuation.separator.arguments +defmodule Module, compile? do end +# ^^^^^^^^ variable.other +# ^ punctuation.separator.arguments +defmodule(Module, compile? do end) +# ^^^^^^^^ variable.other +# ^ punctuation.separator.arguments defmodule :<<>> # ^^^^ entity.name.namespace defmodule :&&, do: def(a &&& b, do: a && b); :&&.&&&(:&, :&) diff --git a/tests/syntax_test_function_calls.ex b/tests/syntax_test_function_calls.ex index 945b2ca6..b97d753f 100644 --- a/tests/syntax_test_function_calls.ex +++ b/tests/syntax_test_function_calls.ex @@ -788,6 +788,59 @@ for CE.source( # ^^^^^^^^^^^^^^^^ variable.other # ^^^ constant.other.keyword +with {:ok, user} <- fetch_user(id), + get_account(user.name, user.email), +# ^^^^^ variable.other.member +# ^^^^ variable.other +# ^^^^ variable.other.member +# ^^^^ variable.other +# ^^^^^^^^^^^^^^^^^^^^^^^ meta.function-call.arguments +# ^^^^^^^^^^^ variable.function + account = get_account(user.name), + name = +# ^ keyword.operator.match +# ^^^^ variable.parameter + user.name, + account = +# ^ keyword.operator.match +# ^^^^^^^ variable.parameter + get_account(user.name, user.email), +# ^^^^^ variable.other.member +# ^^^^ variable.other +# ^ punctuation.separator.arguments +# ^^^^ variable.other.member +# ^^^^ variable.other +# ^^^^^^^^^^^ variable.function + email = +# ^ keyword.operator.match +# ^^^^^ variable.parameter + user.email do +# ^^^^^ variable.other.member +# ^ punctuation.accessor.dot +# ^^^^ variable.other +end + +case variable do + a..b -> identifier +# ^^^^^^^^^^ -variable.parameter +# ^ variable.parameter +# ^^ keyword.operator.range +# ^ variable.parameter + %module{} -> identifier +# ^^^^^^^^^^ -variable.parameter +# ^^ punctuation.section.mapping +# ^^^^^^ variable.parameter + %^module{} -> identifier +# ^^^^^^^^^^ -variable.parameter +# ^^ punctuation.section.mapping +# ^^^^^^ -variable.parameter +# ^ keyword.operator.pin +# FIXME: `y` should not be a function call + x.y -> identifier +# ^^^^^^^^^^ -variable.parameter +# ^ punctuation.accessor.dot +# ^ variable.other + end receive do # ^^ keyword.context.block.do diff --git a/tests/syntax_test_misc.ex b/tests/syntax_test_misc.ex index a4a85bc2..ba84ad10 100644 --- a/tests/syntax_test_misc.ex +++ b/tests/syntax_test_misc.ex @@ -11,6 +11,19 @@ %_{} # ^ punctuation.section.mapping.end # ^ variable.other.unused +#^ punctuation.section.mapping.begin + %@module{} +# ^ punctuation.section.mapping.end +# ^ punctuation.section.mapping.begin +# ^^^^^^ variable.other.constant +# ^ keyword.operator.attribute.elixir +#^ punctuation.section.mapping.begin + %^@module{} +# ^ punctuation.section.mapping.end +# ^ punctuation.section.mapping.begin +# ^^^^^^ variable.other.constant +# ^ keyword.operator.attribute.elixir +# ^ keyword.operator.pin.elixir #^ punctuation.section.mapping.begin %{%{}: :%{}}."%{}" # ^^^ variable.other.member @@ -105,7 +118,7 @@ #^ invalid.illegal.stray-closing-brace (fn -> ) end) -# ^ invalid.illegal.stray-closing-parenthesis +# ^ invalid.illegal.stray-closing-parenthesis fn -> end # ^^^ punctuation.section.block.end keyword.context.block.end # ^^ keyword.operator.arrow @@ -149,8 +162,17 @@ # ^ variable.other -variable.parameter # ^ variable.parameter + fn x when +# ^^^^ keyword.operator.when +# ^ variable.parameter + x -> x end +# ^^^ keyword.context.block.end +# ^ variable.other +# ^ variable.other + fn - [], acc -> acc + [], acc \\ [] -> acc +# ^^ keyword.operator.default invalid.illegal.default-operator # ^ punctuation.separator.sequence x, acc -> [x | acc] # ^ punctuation.separator.sequence @@ -202,14 +224,14 @@ y -> y z -> z # ^ variable.other -variable.parameter # ^^ keyword.operator.arrow - #<- variable.other -variable.parameter + #<- variable.parameter end #^^ punctuation.section.block.end fn -> x; y -> z end # ^^^ punctuation.section.block.end # ^ variable.other # ^^ keyword.operator.arrow -# ^ -variable.parameter +# ^ variable.parameter # ^ variable.other # ^^ keyword.operator.arrow fn x -> x; y -> y end @@ -269,7 +291,7 @@ end -> var -> var # ^^^ variable.other # ^^ keyword.operator.arrow - # ^^^ variable.other + # ^^^ variable.parameter #^^ keyword.operator.arrow expr #^^^ variable.other @@ -582,12 +604,12 @@ end[] #^^^ constant.other.capture & 1/&1 -# ^ -punctuation.definition.capture constant.other.capture -# ^ punctuation.definition.capture constant.other.capture +# ^^ constant.other.capture +# ^ punctuation.definition.capture # ^ keyword.operator.arithmetic -# ^ -punctuation.definition.capture constant.other.capture -# ^ -punctuation.definition.capture -constant.other.capture -#^ punctuation.definition.capture constant.other.capture +# ^ constant.numeric.integer +# ^^^ -punctuation.definition.capture -constant.other.capture +#^ keyword.operator.capture & &1..&2; & &1 .. &2 # ^^ constant.other.capture @@ -609,8 +631,8 @@ end[] # ^^^^^ constant.other.module &:"\"Quoted\"\.Module\\".t() # ^ variable.function +# ^ punctuation.accessor.dot # ^^^^^^^^^^^^^^^^^^^^ constant.other.module -# FIXME: ^ punctuation.accessor.dot &:erlang.apply/2 # ^ punctuation.accessor.arity # ^^^^^ variable.other.capture @@ -638,6 +660,39 @@ end[] # ^^^^ variable.other.member # ^ punctuation.accessor.dot # ^^ constant.other.capture + & &1.func/2 +# ^ constant.numeric.arity +# ^ punctuation.accessor.arity +# ^^^^ variable.other.capture +# ^ punctuation.accessor.dot +# ^^ constant.other.capture +# ^ punctuation.definition.capture + & &1.prop.func/1 +# ^ constant.numeric.arity +# ^ punctuation.accessor.arity +# ^^^^ variable.other.capture +# ^ punctuation.accessor.dot +# ^^^^ variable.other.member +# ^ punctuation.accessor.dot +# ^^ constant.other.capture +# ^ punctuation.definition.capture + + &some.thing/1 +# ^ constant.numeric.arity +# ^ punctuation.accessor.arity +# ^^^^^ variable.other.capture +# ^ punctuation.accessor.dot +# ^^^^ variable.other +#^ keyword.operator.capture + &some.thing.good/1 +# ^ constant.numeric.arity +# ^ punctuation.accessor.arity +# ^^^^ variable.other.capture +# ^ punctuation.accessor.dot +# ^^^^^ variable.other.member +# ^ punctuation.accessor.dot +# ^^^^ variable.other +#^ keyword.operator.capture &<%= num %> # ^ keyword.operator.comparison @@ -659,8 +714,8 @@ end[] # ^^^ keyword.operator.logical # ^^ constant.other.capture # ^^ keyword.operator.comparison -# ^ constant.other.capture -constant.numeric -#^ constant.other.capture +# ^ constant.numeric.integer +#^ keyword.operator.capture &mod.&/1; &Mod.&/1 # ^ variable.other.capture @@ -670,6 +725,15 @@ end[] # ^ punctuation.accessor.dot # ^^^ variable.other + &@module.func/1 +# ^ constant.numeric.arity +# ^ punctuation.accessor.arity +# ^^^^ variable.other.capture +# ^ punctuation.accessor.dot +# ^^^^^^ variable.other.constant +# ^ keyword.operator.attribute +#^ keyword.operator.capture.elixir + &x."a func" # ^^^^^^^^ meta.member # ^ punctuation.accessor.dot @@ -955,6 +1019,13 @@ end[] &unquote(:erlang).apply/2 # ^ punctuation.accessor.arity + &Module.func -&1 +# ^ punctuation.section.arguments.end +# ^^ constant.other.capture +# ^ keyword.operator.arithmetic +# ^ punctuation.section.arguments.begin +# ^^^^ variable.function -variable.other.capture + # Semantically invalid, but it complicates the rules to do it correctly: &./2 ^ variable.other.member -punctuation.accessor.arity @@ -1190,8 +1261,8 @@ fn a,,b -> end # ^ punctuation.section.group.end # ^ invalid.illegal.stray-closing-parenthesis ( fn -> ) end ) -# ^ punctuation.section.group.end -# ^ invalid.illegal.stray-closing-parenthesis +# ^ invalid.illegal.stray-closing-parenthesis +# ^ punctuation.section.group.end [ ( ] ) # ^ invalid.illegal.stray-closing-parenthesis @@ -1212,8 +1283,8 @@ fn a,,b -> end # ^ punctuation.section.brackets.end # ^ invalid.illegal.stray-closing-bracket [ fn -> ] end ] -# ^ punctuation.section.brackets.end -# ^ invalid.illegal.stray-closing-bracket +# ^ invalid.illegal.stray-closing-bracket +# ^ punctuation.section.brackets.end { ( } ) # ^ invalid.illegal.stray-closing-parenthesis @@ -1234,8 +1305,8 @@ fn a,,b -> end # ^ punctuation.section.sequence.end # ^ invalid.illegal.stray-closing-brace { fn -> } end } -# ^ punctuation.section.sequence.end -# ^ invalid.illegal.stray-closing-brace +# ^ invalid.illegal.stray-closing-brace +# ^ punctuation.section.sequence.end %{ ( } ) # ^ invalid.illegal.stray-closing-parenthesis @@ -1256,8 +1327,8 @@ fn a,,b -> end # ^ punctuation.section.mapping.end # ^ invalid.illegal.stray-closing-brace %{ fn -> } end } -# ^ punctuation.section.mapping.end -# ^ invalid.illegal.stray-closing-brace +# ^ invalid.illegal.stray-closing-brace +# ^ punctuation.section.mapping.end << ( >> ) # ^ invalid.illegal.stray-closing-parenthesis @@ -1278,8 +1349,8 @@ fn a,,b -> end # ^^ punctuation.definition.string.end # ^^ invalid.illegal.stray-closing-binary << fn -> >> end >> -# ^^ punctuation.definition.string.end -# ^^ invalid.illegal.stray-closing-binary +# ^^ invalid.illegal.stray-closing-binary +# ^^ punctuation.definition.string.end do ( end ) # ^ invalid.illegal.stray-closing-parenthesis diff --git a/tests/syntax_test_sql.ex.sql b/tests/syntax_test_sql.ex.sql index ed234974..1e285970 100644 --- a/tests/syntax_test_sql.ex.sql +++ b/tests/syntax_test_sql.ex.sql @@ -3,18 +3,18 @@ -- Identifiers SELECT * FROM posts; --- ^ variable.language.wildcard.asterisk +-- ^ constant.other.wildcard.asterisk SELECT posts."column"; -- ^^^^^^ string -- ^^^^^ constant.other.table-name SELECT posts.*; --- ^ variable.language.wildcard.asterisk +-- ^ constant.other.wildcard.asterisk -- ^ punctuation.accessor.dot -- ^^^^^ constant.other.table-name SELECT json_object(posts.*); --- ^ variable.language.wildcard.asterisk +-- ^ constant.other.wildcard.asterisk -- ^ punctuation.accessor.dot -- ^^^^^ constant.other.table-name diff --git a/tests/syntax_test_sql_fragments.ex b/tests/syntax_test_sql_fragments.ex index 02efcea1..d6ca701f 100644 --- a/tests/syntax_test_sql_fragments.ex +++ b/tests/syntax_test_sql_fragments.ex @@ -53,7 +53,7 @@ fragment("\ # ^^ source.ex.sql punctuation.separator.continuation SELECT *\ # ^^ source.ex.sql punctuation.separator.continuation -# ^ variable.language.wildcard.asterisk.sql +# ^ constant.other.wildcard.asterisk.sql # ^^^^^^ keyword.other.DML.sql -- Interpolations are not accepted by fragment(), but we match them anyway: FROM #{:posts} @@ -119,7 +119,7 @@ fragment( # ^^^^^^^^^ meta.string.elixir meta.interpolation.elixir # ^^ keyword.operator.psql # ^^^^^^^^^^^^^^^^^^^^^^^^ variable.function.sql -# ^ variable.language.wildcard.asterisk.sql +# ^ constant.other.wildcard.asterisk.sql # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string.elixir source.ex.sql # ^ punctuation.section.arguments.begin # ^^^^^^^ keyword.other.unquote @@ -153,6 +153,53 @@ fragment("t AT TIME ZONE") # ^^^^ keyword.other.sql # ^^ keyword.other.sql +## SQL queries in `query` and `query_many` + + query("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +#^^^^^ variable.function + query!("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +#^^^^^^ variable.function + query_many("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +#^^^^^^^^^^ variable.function + query_many!("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +#^^^^^^^^^^^ variable.function + Repo.query("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^ variable.function +#^^^^ constant.other.module + Repo.query!("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^^ variable.function +#^^^^ constant.other.module + SQL.query(MyRepo, "SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^ variable.function +#^^^ constant.other.module + SQL.query!(MyRepo, "SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^^ variable.function +#^^^ constant.other.module + MyApp.Repo.query("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^ variable.function +# ^^^^ constant.other.module + MyApp.Repo.query!("SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^^ variable.function +# ^^^^ constant.other.module + MyApp.SQL.query(MyRepo, "SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^ variable.function +# ^^^ constant.other.module + MyApp.SQL.query!(MyRepo, "SELECT * FROM users", []) +# ^^^^^^ keyword.other.DML +# ^^^^^^ variable.function +# ^^^ constant.other.module + ## Raw SQL queries sql("SELECT * FROM posts ORDER BY title GROUP BY user_id") @@ -162,7 +209,7 @@ fragment("t AT TIME ZONE") # ^^^^^ keyword.other.sql # ^^^^^ source.ex.sql # ^^^^ keyword.other.DML.sql -# ^ variable.language.wildcard.asterisk.sql +# ^ constant.other.wildcard.asterisk.sql # ^^^^^^ keyword.other.DML.sql # ^^^^^^^^^^^^^^^^^^^^^ meta.string.elixir #^^^ variable.function @@ -204,7 +251,7 @@ fragment("t AT TIME ZONE") # ^ variable.other.sql # ^^ keyword.operator.assignment.alias.sql # ^^^^^^^ string.quoted.double.sql -# ^ variable.language.wildcard.asterisk.sql +# ^ constant.other.wildcard.asterisk.sql """) sql(""" @@ -285,7 +332,7 @@ fragment("t AT TIME ZONE") # ^^^^^^^ keyword.other.sql # ^^^ keyword.other.sql # ^^^^^ keyword.other.sql -# ^^^^^ constant.language.boolean.sql +# ^^^^^ constant.language.boolean.false.sql # ^^^^^^ keyword.other.sql # ^^^ keyword.other.DML.sql # ^^^^ keyword.other.DML.sql @@ -328,7 +375,7 @@ fragment("t AT TIME ZONE") SOME; SYMMETRIC; TABLE; TABLESAMPLE; THEN; TO; TRAILING; TRUE; UNION; UNIQUE; # ^^^^^^ keyword.other.sql # ^^^^^ keyword.other.DML.sql -# ^^^^ constant.language.boolean.sql +# ^^^^ constant.language.boolean.true.sql # ^^^^^^^^ keyword.other.sql # ^^ keyword.other.sql # ^^^^ keyword.other.DML.sql diff --git a/tests/syntax_test_template.html.heex b/tests/syntax_test_template.html.heex index e4a3c045..7296c10c 100644 --- a/tests/syntax_test_template.html.heex +++ b/tests/syntax_test_template.html.heex @@ -48,6 +48,30 @@ +