Skip to content

Releasing v4.0.0 #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.: `<div {@dynamic_attrs} />`.
- 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.
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion color-schemes/Mariana.sublime-color-scheme
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion color-schemes/Monokai.sublime-color-scheme
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "Monokai for Elixir",
"variables":
{
"white3": "#ddd",
"entity": "var(yellow2)",
"doc": "var(yellow5)"
},
Expand Down Expand Up @@ -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",
Expand Down
35 changes: 18 additions & 17 deletions commands/mix_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
127 changes: 80 additions & 47 deletions commands/mix_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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})

Expand All @@ -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'):
Expand All @@ -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'] = {
Expand All @@ -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
Expand Down
Loading
Loading