From f8154fd24afd478d444a47b963566ed434df2062 Mon Sep 17 00:00:00 2001 From: DeathAxe Date: Sat, 25 Sep 2021 17:55:18 +0200 Subject: [PATCH 01/98] CI: Use fixed revision of default packages for syntax tests --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92b97bbe..a998a8d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,11 +3,13 @@ name: Syntax Tests on: push: paths: + - '.github/workflows/main.yml' - '**.sublime-syntax' - '**/syntax_test*' - '**.tmPreferences' pull_request: paths: + - '.github/workflows/main.yml' - '**.sublime-syntax' - '**/syntax_test*' - '**.tmPreferences' @@ -21,5 +23,5 @@ jobs: - uses: SublimeText/syntax-test-action@v2 with: build: 4113 - default_packages: master + default_packages: v4113 package_name: ElixirSyntax From 95b253ebe6fa7c33e6b0aaca6a5df89956c24512 Mon Sep 17 00:00:00 2001 From: DeathAxe Date: Thu, 23 Sep 2021 18:21:59 +0200 Subject: [PATCH 02/98] HTML (EEx): refactored and improved contexts and scopes. ## Distinguish embedding and interpolation This commit splits existing contexts into two types: 1. `eex-embedded`, which is used by default (prototype) 2. `eex-interpolations`, which is used to interpolate strings The difference between the two is that (1.) does not clear any scopes, while (2.) clears 1 scope (the string scope). Before this commit all tags used outside of strings cleared the main scope of the syntax `text.html.eex`. Test cases are sorted so those testing later tokens in a line follow those which test earlier tokens. ## Embedded punctuation and meta scopes This commit... 1. proposes to scope the whole `<%` and `%>` token as `punctuation.section.embedded`. These are no real tags but rather special markers to specify start and end of embedded Elixir code. It can be compared with PHP, JSP, ASP and various other templating syntaxes, which expand those embedded code blocks while processing the template content. That's why `entity.name.tag` seems not suitable. 2. scopes the embedded code `meta.embedded` because the punctuation already use `punctuation.section.embedded`. 3. scopes embedded code in stings `meta.interpolation meta.embedded`, due to a scope naming guideline suggesting the former scope for embedded code in strings. 3. scopes embedded code `source.elixir.embedded.html` to enable auto-completions. ST requires the `source` scope to do handle content as code rather plain text. 4. removes "atomic groups" as those are not supported by ST's sregex syntax engine (see: `<%(?:%=?|[=/|]?)`) ## Exclude prototypes Avoid including the `prototype` context in embedded code sections as <% ... %> won't be valid inside them self. ## Rethink comments This commit modifies EEx comments so they appear as normal interpolation. This change is inspired by ST's JSX/TSX syntax, which supports comments in JSX components by `{/* comment */}`. The braces are scoped as interpolation punctuation, while only the content `/* ... */` uses comment scope. It appears more accurate with regards to how interpolation works in template languages and may wanted to be applied in other syntaxes as well. ## Don't stack meta.interpolation and meta.embedded Probably not useful nor needed to mix those two in case the distinction between embedding and interpolation is to be maintained. ## Renamed some context names back and appended "-pop" (Aziz) Contexts are named that way as a convention which is kept throughout the Elixir.sublime-syntax file. --- syntaxes/HTML (EEx).sublime-syntax | 88 ++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/syntaxes/HTML (EEx).sublime-syntax b/syntaxes/HTML (EEx).sublime-syntax index 892707cd..f7bcae2f 100644 --- a/syntaxes/HTML (EEx).sublime-syntax +++ b/syntaxes/HTML (EEx).sublime-syntax @@ -6,51 +6,83 @@ scope: text.html.eex extends: Packages/HTML/HTML.sublime-syntax file_extensions: [html.eex, html.leex] -authors: [Aziz Köksal ] +authors: + - Aziz Köksal + - deathaxe contexts: # HTML: prototype: - meta_prepend: true - - include: eex-tags + - include: eex-embedded tag-attribute-value-content: - meta_prepend: true - - include: eex-tags + - include: eex-interpolations strings-common-content: - meta_prepend: true - - include: eex-tags + - include: eex-interpolations # EEx: - eex-tags: - - match: <%# - scope: punctuation.definition.comment.begin.eex - push: - - meta_scope: text.html.eex comment.block.eex - - match: '%>' - scope: punctuation.definition.comment.end.eex - pop: 1 + eex-interpolations: + - match: (<%)(#) + captures: + 1: punctuation.section.embedded.begin.eex + 2: comment.block.eex punctuation.definition.comment.begin.eex + push: eex-interpolation-comment-content-pop # Tags <%/ and <%| are parsed but have no functionality in EEx yet. - - match: (<)(%(?>%=?|[=/|]?)) + - match: <%(?:%=?|[=/|]?) + scope: punctuation.section.embedded.begin.eex + push: eex-interpolation-tag-content-pop + + eex-interpolation-comment-content-pop: + # Need to clear the string scope so that auto-completion works, + # which is disabled by default in strings and comments. + - clear_scopes: 1 + - meta_include_prototype: false + - meta_scope: meta.embedded.eex + - meta_content_scope: comment.block.eex + - include: eex-comment-content-pop + + eex-interpolation-tag-content-pop: + # Need to clear the string scope. + - clear_scopes: 1 + - meta_include_prototype: false + - meta_scope: meta.embedded.eex + - meta_content_scope: source.elixir.embedded.html + - include: eex-embedded-content-pop + + eex-embedded: + - match: (<%)(#) captures: 1: punctuation.section.embedded.begin.eex - 2: entity.name.tag.eex - push: - - meta_scope: meta.interpolation.eex text.html.eex - - clear_scopes: 1 - - include: eex-closing-tag-punctuation-pop - # - include: scope:source.elixir.eex - - include: scope:source.elixir - apply_prototype: true - - eex-closing-tag-punctuation-pop: - - match: (%)(>) - scope: text.html.eex - captures: - 1: entity.name.tag.eex - 2: punctuation.section.embedded.end.eex + 2: comment.block.eex punctuation.definition.comment.begin.eex + push: eex-comment-content-pop + + # Tags <%/ and <%| are parsed but have no functionality in EEx yet. + - match: <%(?:%=?|[=/|]?) + scope: punctuation.section.embedded.begin.eex + push: eex-embedded-content-pop + + eex-comment-content-pop: + - meta_include_prototype: false + - meta_scope: meta.embedded.eex + - meta_content_scope: comment.block.eex + - include: eex-embedded-end-pop + + eex-embedded-content-pop: + - meta_include_prototype: false + - meta_scope: meta.embedded.eex + - meta_content_scope: source.elixir.embedded.html + - include: eex-embedded-end-pop + - include: scope:source.elixir + apply_prototype: true + + eex-embedded-end-pop: + - match: '%>' + scope: punctuation.section.embedded.end.eex pop: 1 From 1fd6d7c4db0f085153d463d1a9ed96df09ea3b3e Mon Sep 17 00:00:00 2001 From: DeathAxe Date: Thu, 23 Sep 2021 19:21:24 +0200 Subject: [PATCH 03/98] HTML (HEEx): refactored and improved contexts and scopes. ## Re-use contexts from EEx This commit... 1. re-uses `eex-embedded` context in HTML (HEEx).sublime-syntax 2. copies over required test cases 3. sorts test cases the same way as in previous commits. ## Distinguish elixir interpolations This commit separates normal interpolation from string interpolation to correctly clear string scopes but don't do so for unwanted maybe top-level scopes. ## Add missing meta.tag scope Some color schemes rely on `meta.tag` to scope attribute values correctly. ## Re-use some existing HTML contexts This commit... 1. replaces some explicit patterns by already existing contexts, derived from inherited HTML.sublime-syntax or HTML (Plain).sublime-syntax 2. Removes unnecessary `apply_prototype` as it hasn't any effect when including contexts from the same (this) syntax definition. ## Rename some contexts Many contexts pop, so naming all of them `-pop` is probably somewhat overwhelming. Here's a suggestion which proved quite useful to express popping contexts while keeping names clean. 1. Non-popping contexts use plural names (e.g. "tags") 2. Popping contexts use singular name (e.g. "tag-content") Note: HTML.sublime-syntax unfortunately doesn't follow this naming scheme consequently for historical reasons and won't do so anytime soon in order to not break existing 3rd party packages. Any new syntax should probably do so though. ## Remove unsupported regexp patterns Atomic groups and possessive quantifiers are unsupported by sregex engine and may either have no effect or trigger slower Oniguruma. Please: check if this has negative effects. ## Simplify tag-name contexts Context switches are not needed and some patterns can be shared. ## Avoid duplicate tag name assignments Avoiding capture groups can improve parsing performance. ST is quite good with regards to re-using lookahead results, so they the overall result is at least equal or even slightly faster with regards to performance, while code looks somewhat cleaner. ## Rename variables Some `tag_...` variables already exist in HTML.sublime-syntax and HTML (Plain).sublime-syntax. This commit makes sure to not regress something due to overriding some HTML related variables by accident. Note: All new variables and contexts should be prefixed. ## Prepend HEEx tags to tag-other Only low priority, but HEEx tags are basically foreign tags with regards to HTML specs. As such those should be placed in `tag-other` context. It ensures any valid html tag is matched correctly under all circumstances. It would even work if foreign tags are lower-case. ## Renamed some context names back and appended "-pop" (Aziz) Contexts are named that way as a convention which is kept throughout the Elixir.sublime-syntax file. --- syntaxes/HTML (HEEx).sublime-syntax | 182 +++++++++++----------------- 1 file changed, 72 insertions(+), 110 deletions(-) diff --git a/syntaxes/HTML (HEEx).sublime-syntax b/syntaxes/HTML (HEEx).sublime-syntax index f80764e1..ad6003dc 100644 --- a/syntaxes/HTML (HEEx).sublime-syntax +++ b/syntaxes/HTML (HEEx).sublime-syntax @@ -6,143 +6,105 @@ scope: text.html.heex extends: Packages/HTML/HTML.sublime-syntax file_extensions: [heex] -authors: [Aziz Köksal ] +authors: + - Aziz Köksal + - deathaxe variables: - tag_char: (?:[^ \n."'/=<>\x{7F}-\x{9F}]) - upcase_tag_name: '[A-Z]{{tag_char}}*' + heex_tag_char: '[^ \n."''/=<>\x{7F}-\x{9F}]' + heex_tag_name: '[A-Z]{{heex_tag_char}}*' + is_heex_tag_name_begin: (?=[.A-Z]) contexts: # HTML overrides: - tag: + tag-other: - meta_prepend: true - - include: heex-html-tag - - include: heex-tag + - include: heex-tags + - include: HTML (EEx).sublime-syntax#eex-embedded - tag-other-name: + tag-generic-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - tag-attributes: + tag-class-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - tag-attribute-value-content: + tag-event-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - strings-common-content: + tag-href-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - comment-content: + tag-id-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop + + tag-style-attribute-value: + - meta_prepend: true + - include: elixir-embedded-pop # HEEx: - heex-html-tag: - - match: (<)(?>(?=\.)|({{upcase_tag_name}})) - captures: - 1: punctuation.definition.tag.begin.html - 2: entity.name.tag.begin.heex - push: [heex-begin-tag-rest-pop, heex-begin-tag-name-rest-pop] - - - match: ((?=\.)|({{upcase_tag_name}})) - captures: - 1: punctuation.definition.tag.begin.html - 2: entity.name.tag.end.heex - push: [heex-end-tag-rest-pop, heex-end-tag-name-rest-pop] - - heex-begin-tag-rest-pop: - - match: /?> - scope: punctuation.definition.tag.end.html - pop: 1 + heex-tags: + - match: <{{is_heex_tag_name_begin}} + scope: punctuation.definition.tag.begin.html + push: [heex-begin-tag-content-pop, heex-begin-tag-name-pop] + + - match: |(?=\S) - scope: punctuation.definition.tag.end.html - pop: 1 + heex-end-tag-content-pop: + - meta_scope: meta.tag.other.heex + - include: tag-end + - include: else-pop - heex-begin-tag-name-rest-pop: - - match: \. - scope: punctuation.accessor.dot.heex - push: - - match: (?>[[:lower:]_]\w*+[?!]?+) - scope: variable.function.heex - pop: 2 - - match: '{{upcase_tag_name}}|(?=\S)' - scope: entity.name.tag.begin.heex - pop: 1 - - match: (?=\S) - pop: 1 + heex-begin-tag-name-pop: + - match: '{{heex_tag_name}}' + scope: entity.name.tag.begin.heex + - include: heex-tag-name-common-pop - heex-end-tag-name-rest-pop: - - match: \. - scope: punctuation.accessor.dot.heex - push: - - match: (?>[[:lower:]_]\w*+[?!]?+) - scope: variable.function.heex - pop: 2 - - match: '{{upcase_tag_name}}|(?=\S)' - scope: entity.name.tag.end.heex - pop: 1 - - match: (?=\S) - pop: 1 + heex-end-tag-name-pop: + - match: '{{heex_tag_name}}' + scope: entity.name.tag.end.heex + - include: heex-tag-name-common-pop - heex-tag: - - match: <%# - scope: punctuation.definition.comment.begin.eex - push: - - meta_scope: text.html.eex comment.block.eex - - match: '%>' - scope: punctuation.definition.comment.end.eex - pop: 1 - - # - match: (<)(%%=) - # captures: - # 1: punctuation.section.embedded.begin.eex - # 2: entity.name.tag.eex - # push: - # - meta_scope: meta.interpolation.eex text.html.eex - # - clear_scopes: 1 - # - include: heex-closing-tag-punctuation-pop - # - include: scope:source.elixir.eex - # apply_prototype: true - - # Tags <%/ and <%| are parsed but have no functionality in EEx yet. - - match: (<)(%(?>%=?|[=/|]?)) - captures: - 1: punctuation.section.embedded.begin.eex - 2: entity.name.tag.eex - push: - - meta_scope: meta.interpolation.eex text.html.eex - - clear_scopes: 1 - - include: heex-closing-tag-punctuation-pop - - include: scope:source.elixir - apply_prototype: true - - heex-closing-tag-punctuation-pop: - - match: (%)(>) - scope: text.html.eex - captures: - 1: entity.name.tag.eex - 2: punctuation.section.embedded.end.eex + heex-tag-name-common-pop: + - match: '[[:lower:]_]\w*[?!]?' + scope: variable.function.heex pop: 1 + - match: \. + scope: punctuation.accessor.dot.heex + - include: immediately-pop # Elixir: - elixir-interpolated: + elixir-embedded: + - match: (?={) + push: elixir-embedded-pop + + elixir-embedded-pop: - match: \{ - scope: punctuation.section.interpolation.begin.elixir - push: - - clear_scopes: 1 - - meta_scope: meta.interpolation.html source.elixir.interpolated.html - - match: \} - scope: punctuation.section.interpolation.end.elixir - pop: 1 - - include: scope:source.elixir - apply_prototype: true + scope: punctuation.section.embedded.begin.elixir + set: elixir-embedded-content-pop + + elixir-embedded-content-pop: + - meta_scope: meta.embedded.heex + - meta_content_scope: source.elixir.embedded.html + - include: elixir-embedded-content-nometa-pop + + elixir-embedded-content-nometa-pop: + - match: \} + scope: punctuation.section.embedded.end.elixir + pop: 1 + - include: scope:source.elixir + apply_prototype: true From f1ba6f03ee7783ab2e65c78745569634bd3cf53e Mon Sep 17 00:00:00 2001 From: DeathAxe Date: Fri, 24 Sep 2021 16:51:33 +0200 Subject: [PATCH 04/98] HTML (Surface): refactored and improved contexts and scopes. ## Re-use elixir interpolation from HEEx This commit... 1. reduces duplicate context definitions 2. ensures same interpolation/embedded scopes are used across syntaxes ## Prepend surface tags to tag-other Only low priority, but surface tags are basically foreign tags with regards to HTML specs. As such those should be placed in `tag-other` context. It ensures any valid html tag is matched correctly under all circumstances. It would even work if foreign tags are lower-case. ## Embedded surface comments This commit... 1. adds named context for surface comment content as those help debugging syntaxes via ST's scope name popup. 2. removes `clear_scopes: 1` as it removed the main scope. ## Rename variables Some `tag_...` variables already exist in HTML.sublime-syntax and HTML (Plain).sublime-syntax. This commit makes sure to not regress something due to overriding some HTML related variables by accident. Note: All new variables and contexts should be prefixed. ## Add missing meta.tag scopes Some color schemes rely on `meta.tag` to scope attribute values correctly. ## Rename some contexts Many contexts pop, so naming all of them `-pop` is probably somewhat overwhelming. Here's a suggestion which proved quite useful to express popping contexts while keeping names clean. 1. Non-popping contexts use plural names (e.g. "tags") 2. Popping contexts use singular name (e.g. "tag-content") Goal ist to follow the introduced scheme of HEEx Note: HTML.sublime-syntax unfortunately doesn't follow this naming scheme consequently for historical reasons and won't do so anytime soon in order to not break existing 3rd party packages. Any new syntax should probably do so though. ## Reorganize Markdown/Raw tag contexts This commit just moves markdown and raw contexts to group them with the surface-other contexts. The idea is the following structure: 1. extended HTML contexts 2. surface tag contexts 3. surface blocks and embedding contexts 4. eex interpolation ## Simplify tag-name contexts Context switches are not needed and some patterns can be shared. ## Remove unsupported regexp patterns Atomic groups and possessive quantifiers are unsupported by sregex engine and may either have no effect or trigger slower Oniguruma. Please: check if this has negative effects. ## Add named context for surface-blocks ## Simplify conditional block pattern ## Don't clear comment scope in interpolation Follow HEEx implementation. ## Simplify html-custom-tags This commit... 1. removes the group (?:) from `surface_tag_char` variable as it seems unnecessary to wrap a char-set this way. 2. adds a `html_custom_tag_char` custom variable which is a copy of `surface_tag_char` but excludes the period. This new variable allows to replace the groups from the patterns in the `html-custom-tags` context by a simple char-set pattern. 3. As the pattern in `html-custom-tags` matches every valid tag character, it is not required to push into `surface-begin-tag-name` context as it doesn't have to offer anything more. It just pops immediately. 4. `html-custom-tags` basically overrides `tag-other` from HTML.sublime-syntax in a more general way by not requiring a tag name to start with an ascii char. Thus `meta_prepend: true` can be removed from `tag-other`. The pattern from HTML wouldn't match anyway. ## Renamed some context names back and appended "-pop" (Aziz) Contexts are named that way as a convention which is kept throughout the Elixir.sublime-syntax file. --- syntaxes/HTML (Surface).sublime-syntax | 349 ++++++++++++------------- 1 file changed, 170 insertions(+), 179 deletions(-) diff --git a/syntaxes/HTML (Surface).sublime-syntax b/syntaxes/HTML (Surface).sublime-syntax index 20dd6035..786a5242 100644 --- a/syntaxes/HTML (Surface).sublime-syntax +++ b/syntaxes/HTML (Surface).sublime-syntax @@ -6,254 +6,245 @@ scope: text.html.surface extends: Packages/HTML/HTML.sublime-syntax file_extensions: [sface] -authors: [Aziz Köksal ] +authors: + - Aziz Köksal + - deathaxe variables: - tag_char: (?:[^ \n."'/=<>\x{7F}-\x{9F}]) - tag_name: '[a-zA-Z]{{tag_char}}*' - upcase_tag_name: '[A-Z]{{tag_char}}*' + html_custom_tag_char: '[^ \n"''/=<>\x{7F}-\x{9F}]' + surface_tag_char: '[^ \n."''/=<>\x{7F}-\x{9F}]' + surface_tag_name: '[a-zA-Z]{{surface_tag_char}}*' + surface_upcase_tag_name: '[A-Z]{{surface_tag_char}}*' contexts: # HTML overrides: - tag: + tag-other: + - include: surface-markdown-tag + - include: surface-raw-tag + - include: surface-other-tags + - include: html-custom-tags + - include: surface-blocks + - include: surface-comment + - include: elixir-embedded + + tag-attributes: - meta_prepend: true - - include: surface-tag - - include: surface-block - - include: surface-private-comment + - include: surface-attributes - include: elixir-embedded - tag-other: - - meta_append: true - - include: html-custom-tag + tag-generic-attribute-value: + - meta_prepend: true + - include: elixir-embedded-pop - tag-other-name: + tag-class-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - tag-attributes: + tag-event-attribute-value: - meta_prepend: true - - include: elixir-interpolated - - include: surface-attributes + - include: elixir-embedded-pop - tag-attribute-value-content: + tag-href-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - strings-common-content: + tag-id-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop - comment-content: + tag-style-attribute-value: - meta_prepend: true - - include: elixir-interpolated + - include: elixir-embedded-pop + + # Surface: - # Surface & Elixir: + surface-markdown-tag: + # NB: the #Markdown-tag cannot be nested, e.g.: `<#Markdown><#Markdown>` + - match: (<)(#Markdown)\b + captures: + 1: punctuation.definition.tag.begin.html + 2: entity.name.tag.begin.surface + push: surface-markdown-tag-content-pop - surface-tag: - - include: surface-raw - - include: surface-markdown + surface-markdown-tag-content-pop: + - meta_scope: meta.tag.other.surface + - match: \> + scope: punctuation.definition.tag.end.html + set: surface-markdown-body-pop + - include: tag-end-self-closing + - include: tag-attributes - - match: (<)([#:]{{tag_name}}|{{upcase_tag_name}}) + surface-markdown-body-pop: + - match: (<#Raw>` + - match: (<)(#Raw)(>) + captures: + 0: meta.tag.other.surface + 1: punctuation.definition.tag.begin.html + 2: entity.name.tag.begin.surface + 3: punctuation.definition.tag.end.html + push: surface-raw-body-pop + + surface-raw-body-pop: + - match: ({{tag_char}}|\.)+) + html-custom-tags: + - match: (<)({{html_custom_tag_char}}+) captures: 1: punctuation.definition.tag.begin.html 2: entity.name.tag.begin.html - push: [surface-begin-tag-rest-pop, surface-begin-tag-name-rest-pop] + push: surface-begin-tag-content-pop - - match: ({{tag_char}}|\.)+) + - match: ( - scope: punctuation.definition.tag.end.html - pop: 1 + surface-begin-tag-content-pop: + - meta_scope: meta.tag.other.surface + - include: tag-end-maybe-self-closing - include: tag-attributes - apply_prototype: true - surface-end-tag-rest-pop: - - match: \>|(?=\S) - scope: punctuation.definition.tag.end.html - pop: 1 + surface-end-tag-content-pop: + - meta_scope: meta.tag.other.surface + - include: tag-end + - include: else-pop - surface-begin-tag-name-rest-pop: - - match: \. - scope: punctuation.accessor.dot.surface - push: - - match: (?>[[:lower:]_]\w*+[?!]?+) - scope: variable.function.surface - pop: 2 - - match: '{{upcase_tag_name}}|(?=\S)' - scope: entity.name.tag.begin.surface - pop: 1 - - match: (?=\S) - pop: 1 + surface-begin-tag-name-pop: + - match: '{{surface_upcase_tag_name}}' + scope: entity.name.tag.begin.surface + - include: surface-tag-name-common-pop + + surface-end-tag-name-pop: + - match: '{{surface_upcase_tag_name}}' + scope: entity.name.tag.end.surface + - include: surface-tag-name-common-pop - surface-end-tag-name-rest-pop: + surface-tag-name-common-pop: + - match: '[[:lower:]_]\w*[?!]?' + scope: variable.function.surface + pop: 1 - match: \. scope: punctuation.accessor.dot.surface - push: - - match: (?>[[:lower:]_]\w*+[?!]?+) - scope: variable.function.surface - pop: 2 - - match: '{{upcase_tag_name}}|(?=\S)' - scope: entity.name.tag.end.surface - pop: 1 - - match: (?=\S) - pop: 1 + - include: immediately-pop surface-attributes: - match: |- (?x) - (:)((?> + (:)( if | hook | show | let | args | values - | on-(?> + | on-(?: click | capture-click | blur | focus | change | submit | keydown | keyup - | window-(?>focus | blur | keydown | keyup) + | window-(?:focus | blur | keydown | keyup) ) - )){{attribute_name_break}} + ){{attribute_name_break}} captures: 1: punctuation.definition.attribute.begin.surface 2: entity.other.attribute-name.surface - push: [tag-id-attribute-meta, tag-id-attribute-assignment] + push: [tag-generic-attribute-meta, tag-generic-attribute-assignment] - surface-block: - - match: ({/)([a-z]+)(}) - scope: meta.embedded.html meta.block.tag.end.surface source.elixir.embedded.html + surface-blocks: + - match: ({/)(?:(case|elseif|else|if|unless)|(for)|([a-z]+))(}) captures: - 1: punctuation.definition.tag.begin.surface - 2: entity.name.tag.block.surface - 3: punctuation.definition.tag.end.surface + 0: meta.embedded.surface + 1: punctuation.section.embedded.begin.surface + 2: keyword.control.conditional.surface + 3: keyword.control.loop.surface + 4: keyword.control.surface + 5: punctuation.section.embedded.end.surface - match: ({#)(match)\b captures: - 1: punctuation.definition.tag.begin.surface - 2: entity.name.tag.block.surface - push: - - meta_scope: meta.embedded.html meta.block.tag.begin.surface source.elixir.embedded.html - - include: surface-block-closing-brace-pop - - include: scope:source.elixir#parameters - apply_prototype: true - - match: ({#)(?>(else\b)|(elseif\b|[a-z]+))\b - captures: - 1: punctuation.definition.tag.begin.surface - 2: entity.name.tag.block.surface - 3: entity.name.tag.block.surface - # 3: variable.function.surface - push: - - meta_scope: meta.embedded.html meta.block.tag.begin.surface source.elixir.embedded.html - - include: surface-block-closing-brace-pop - - include: scope:source.elixir#arguments_ws - apply_prototype: true - - surface-block-closing-brace-pop: - - match: \} - scope: punctuation.definition.tag.end.surface - pop: 1 - - surface-raw: - # NB: the #Raw-tag cannot be nested, e.g.: `<#Raw><#Raw>` - - match: (<)(#Raw)(>) - captures: - 1: punctuation.definition.tag.begin.html - 2: entity.name.tag.begin.surface - 3: punctuation.definition.tag.end.html - push: surface-raw-body-pop - - surface-raw-body-pop: - - match: (<#Markdown>` - - match: (<)(#Markdown)\b - captures: - 1: punctuation.definition.tag.begin.html - 2: entity.name.tag.begin.surface - push: [surface-markdown-attrs-pop] - - surface-markdown-attrs-pop: - - match: \> - scope: punctuation.definition.tag.end.html - set: surface-markdown-body-pop - - match: /\> - scope: punctuation.definition.tag.end.html - pop: 1 - - include: tag-attributes - - surface-markdown-body-pop: - - match: ( Date: Tue, 5 Oct 2021 20:40:11 +0200 Subject: [PATCH 05/98] Elixir (EEx): refactored and improved contexts and scopes. Follow the modifications done to the HTML (...) syntax files. --- syntaxes/Elixir (EEx).sublime-syntax | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/syntaxes/Elixir (EEx).sublime-syntax b/syntaxes/Elixir (EEx).sublime-syntax index 08248232..7eef607b 100644 --- a/syntaxes/Elixir (EEx).sublime-syntax +++ b/syntaxes/Elixir (EEx).sublime-syntax @@ -18,10 +18,14 @@ contexts: - include: eex_end_tag eex_begin_tag: - - match: (<)(%(?>%=?|[=/|]?)) + - match: (<%)(#) captures: - 1: punctuation.section.embedded.begin.ex.eex - 2: entity.name.tag.ex.eex + 1: punctuation.section.embedded.begin.eex + 2: comment.block.eex punctuation.definition.comment.begin.eex + push: HTML (EEx).sublime-syntax#eex-interpolation-comment-content-pop + + - match: <%(?:%=?|[=/|]?) + scope: punctuation.section.embedded.begin.eex # # NB: causes "context sanity limit" error. # push: core_syntax # with_prototype: @@ -29,7 +33,6 @@ contexts: # pop: true eex_end_tag: - - match: \s*(%)(>) + - match: \s*(%>) captures: - 1: entity.name.tag.ex.eex - 2: punctuation.section.embedded.end.ex.eex + 1: punctuation.section.embedded.end.eex From d2738acdf11aabaadf1123f994cadedb4df34790 Mon Sep 17 00:00:00 2001 From: DeathAxe Date: Fri, 24 Sep 2021 18:55:40 +0200 Subject: [PATCH 06/98] Tests: fixes and adaptations due to new changes. --- tests/syntax_test_strings.ex | 2 +- tests/syntax_test_surface.ex | 4 +- tests/syntax_test_template.ex.eex | 36 +++--- tests/syntax_test_template.html.eex | 80 +++++++----- tests/syntax_test_template.html.heex | 120 +++++++++++++----- tests/syntax_test_template.sface | 179 +++++++++++++++++++-------- 6 files changed, 290 insertions(+), 131 deletions(-) diff --git a/tests/syntax_test_strings.ex b/tests/syntax_test_strings.ex index 14e35dbf..7411fa12 100644 --- a/tests/syntax_test_strings.ex +++ b/tests/syntax_test_strings.ex @@ -181,7 +181,7 @@ heredoc text # ^ -string #^^^ entity.name.tag.block.any.html # ^^^^^^ meta.string.elixir -# ^^^^^^^^^^^ meta.interpolation.eex +# ^^^^^^^^^^^ meta.embedded.eex #^^^^ meta.string.elixir \"""m # ^ -storage.type.string diff --git a/tests/syntax_test_surface.ex b/tests/syntax_test_surface.ex index e0737d66..e91418d2 100644 --- a/tests/syntax_test_surface.ex +++ b/tests/syntax_test_surface.ex @@ -7,7 +7,9 @@ # ^^^ meta.string.elixir punctuation.definition.string.begin.elixir -# ^^^ source.elixir.interpolated.html +# ^ punctuation.section.embedded.end.elixir - source.elixir.embedded +# ^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir - source.elixir.embedded # ^ entity.other.attribute-name.html # ^^^^^^^^^ entity.name.tag.begin.surface # ^^^ entity.name.tag.begin.surface diff --git a/tests/syntax_test_template.ex.eex b/tests/syntax_test_template.ex.eex index d7662a6c..4b61ed47 100644 --- a/tests/syntax_test_template.ex.eex +++ b/tests/syntax_test_template.ex.eex @@ -18,15 +18,15 @@ defmodule <%= @module %>.View do # ^^^^ entity.name.namespace # ^ punctuation.accessor.dot -# ^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex # ^ keyword.operator.attribute -# ^^ entity.name.tag.ex.eex +# ^^^ punctuation.section.embedded.begin.eex end alias <%= @web_namespace %>.Router.Helpers, as: Routes # ^^^^^^ entity.name.namespace # ^ punctuation.accessor.dot -# ^^ entity.name.tag.ex.eex +# ^^^ punctuation.section.embedded.begin.eex :<%= @key %> #<- constant.other.symbol punctuation.definition.constant.begin @@ -41,50 +41,48 @@ alias <%= @web_namespace %>.Router.Helpers, as: Routes # are also highlighted inside strings. # FIXME: make negative check with "-entity" when solved. "<%= string %>" -# ^ entity.name.tag.ex.eex -# ^^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex +#^^^ punctuation.section.embedded.begin.eex M1.<%= M2 %>.f() # ^ punctuation.section.arguments.end # ^ punctuation.section.arguments.begin # ^ variable.function # ^ punctuation.accessor.dot -# ^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex # ^^ constant.other.module -# ^^ entity.name.tag.ex.eex +# ^^^ punctuation.section.embedded.begin.eex # ^ punctuation.accessor.dot x.<%= :member %>() # ^ punctuation.section.arguments.end # ^ punctuation.section.arguments.begin -# ^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex # ^^^^^^^ constant.other.symbol -# ^^ entity.name.tag.ex.eex +# ^^^ punctuation.section.embedded.begin.eex #^ punctuation.accessor.dot ^ punctuation.section.arguments.end ^ punctuation.section.arguments.begin @type <%= :t %> :: any -# ^ entity.name.tag.ex.eex -# ^^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex +# ^^^ punctuation.section.embedded.begin.eex ^^^ support.type @spec <%= :name %>(any) # ^^^ support.type -# ^ entity.name.tag.ex.eex -# ^^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex +# ^^^ punctuation.section.embedded.begin.eex ^ punctuation.definition.parameters.end ^ punctuation.definition.parameters.begin &<%= %> -# ^ entity.name.tag.ex.eex -# ^^ entity.name.tag.ex.eex +# ^^ punctuation.section.embedded.end.eex +# ^^^ punctuation.section.embedded.begin.eex #^ keyword.operator.capture # <%= @web_app_name %> -# ^ punctuation.section.embedded.end.ex.eex -# ^ entity.name.tag.ex.eex -# ^^ entity.name.tag.ex.eex -# ^ punctuation.section.embedded.begin.ex.eex +# ^^ punctuation.section.embedded.end.eex +# ^^^ punctuation.section.embedded.begin.eex #^^^^^^^^^^^^^^^^^^^^^^ comment.line.number-sign.elixir #<- comment.line.number-sign.elixir punctuation.definition.comment.elixir diff --git a/tests/syntax_test_template.html.eex b/tests/syntax_test_template.html.eex index 53089896..f0b7b5ee 100644 --- a/tests/syntax_test_template.html.eex +++ b/tests/syntax_test_template.html.eex @@ -3,56 +3,76 @@ "> + + + + + - - - - + + + - <%# Comment %> - - - - + <%# Comment + + + + + Block + + %> + + + +
+ + + + + <% func arg %> - - + + + + - - - + <%= if true? do %> - - + + + + - - + <% end %> - - + + + + - - + <%% quoted :code %> - - + + + + - - + <%%= quoted :result %> - - + + + + - - + diff --git a/tests/syntax_test_template.html.heex b/tests/syntax_test_template.html.heex index 14a8e136..2ee7023e 100644 --- a/tests/syntax_test_template.html.heex +++ b/tests/syntax_test_template.html.heex @@ -2,18 +2,51 @@ + - - - + + + + + + + + + + + <%# Comment %> - - - - + + + + + + + + +
+ + +

+ + + + + + + + + + + + + <.form> @@ -22,58 +55,87 @@ + + + + + + + - + + + + + + + + + + + + + + + + + + <{=@a}a /> + <% func arg %> - - + + + + - - - + <%= if true? do %> - - + + + + - - + <% end %> - - + + + + - - + <%% quoted :code %> - - + + + + - - + <%%= quoted :result %> - - + + + + - - + diff --git a/tests/syntax_test_template.sface b/tests/syntax_test_template.sface index 6cb9998a..8a98919a 100644 --- a/tests/syntax_test_template.sface +++ b/tests/syntax_test_template.sface @@ -1,55 +1,97 @@ # SYNTAX TEST "HTML (Surface).sublime-syntax" + +# ^ invalid +# ^ invalid +# ^^^^^^^^ - meta.embedded +# ^^^^^^ meta.embedded.surface + <%# No EEx comment %> +# ^^^^^^^^^^^^^^^^^^^^^ meta.tag.other.surface - meta.embedded - comment + +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.embedded - comment +# ^^^^^^^^^^^^^^^^^^^^ meta.string.html - meta.embedded - comment + + # ^^^^^^^^^^ string.quoted.double # ^ punctuation.separator.key-value # ^^ entity +# ^ meta.embedded.surface punctuation.section.embedded.end.elixir - source # ^^^^ constant.other.keyword # ^^^^^^ constant.other.keyword -# ^^^^^^^^^^^^^^^^^^^^^^^^ source.elixir.interpolated.html +# ^^^^^^^^^^^^^^^^^^^^^^ meta.embedded.surface source.elixir.embedded.html +# ^ meta.embedded.surface punctuation.section.embedded.begin.elixir - source +# ^ punctuation.definition.attribute.begin.surface #^^^^^^ entity.name.tag.begin.surface {#for i <- 1..max} -# ^^^^^^^^^^^^ meta.function-call.arguments.elixir -# ^^^ entity.name.tag.block.surface -# ^^^^^^^^^^^^^^^^^^ source.elixir.embedded.html - +# ^^^^^^^^^^^^ source.elixir.embedded.html meta.function-call.arguments.elixir +# ^^^ keyword.control.loop.surface +# ^^^^^^^^^^^^^^^^^^ meta.embedded.surface + +# ^^^^^^^^ meta.attribute-with-value.style.html meta.embedded.surface +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.attribute-with-value.class.html meta.embedded.surface +# ^^^^^ meta.attribute-with-value.id.html meta.embedded.surface +# ^^ meta.tag.inline.any.html meta.attribute-with-value entity.other.attribute-name -# ^^^^^^^^^ variable.other.constant.elixir -# ^ keyword.operator.attribute.elixir -# ^^^^^^^^^^^^ source.elixir.interpolated.html +# ^ meta.attribute-with-value.class.html meta.string.html - meta.embedded +# ^ - punctuation.section.embedded.end.elixir - source +# ^^^^^^^^^ - variable.other.constant.elixir +# ^ - keyword.operator.attribute.elixir +# ^^^^^^^^^^ - source +# ^ - punctuation.section.embedded.begin.elixir - source +# ^^^^^^^^^^^^^^^ meta.attribute-with-value.class.html meta.class-name.html meta.string.html - meta.embedded.surface +# ^^^^^^^^ meta.attribute-with-value.class.html meta.string.html - meta.embedded {i + 1} -# ^^^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.end.elixir - source +# ^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir - source +# ^^^^^^^ meta.embedded.surface {#else} -# ^^^^ entity.name.tag.block.surface +# ^^^^ keyword.control.conditional.surface {#elsei} -# ^^^^^ entity.name.tag.block.surface +# ^^^^^ keyword.control.surface {#elseif x == 0} +# ^ punctuation.section.embedded.end.surface # ^^^^^^ source.elixir.embedded.html -# ^^^^^^ entity.name.tag.block.surface +# ^^^^^^ keyword.control.conditional.surface +# ^^ punctuation.section.embedded.begin.surface +# ^^^^^^^^^^^^^^^^ meta.embedded.surface {/for} -# ^^^ entity.name.tag.block.surface +# ^ punctuation.section.embedded.end.surface +# ^^^ keyword.control.loop.surface +# ^^ punctuation.section.embedded.begin.surface +# ^^^^^^ meta.embedded.surface # ^^^^^^ entity.name.tag.end.surface -# ^^^^^^^^^ variable.function.surface -entity.name +# ^^^^^^^^^ variable.function.surface - entity.name +#^^^^^^^^^^^^^^^^^^^^^^^ meta.tag.other.surface # ^^^^^^^^^ entity.name.tag.begin.surface # ^ punctuation.accessor.dot.surface # ^^^ entity.name.tag.begin.surface # ^ punctuation.accessor.dot.surface #^^^^ entity.name.tag.begin.surface +#^^^^^^^^^^^^^^^^^^^^^^^^^ meta.tag.other.surface {#case @value} +# ^^^^^ variable.other.constant.elixir +# ^^^^ keyword.control.conditional.surface +# ^ punctuation.section.embedded.end.surface +# ^^punctuation.section.embedded.begin.surface +# ^^^^^^^^^^^^^^ meta.embedded.surface {#match [{_, first} | _]} -# ^ punctuation.definition.tag.end.surface +# ^ punctuation.section.embedded.end.surface # ^ punctuation.section.sequence.end.elixir # ^^^^^ variable.parameter.elixir # ^ punctuation.section.sequence.begin.elixir -# ^^ punctuation.definition.tag.begin.surface +# ^^ punctuation.section.embedded.begin.surface First {first} -# ^^^^^ variable.other.elixir -# ^^^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.end.elixir - source +# ^^^^^ source.elixir.embedded.html variable.other.elixir +# ^ punctuation.section.embedded.begin.elixir - source +# ^^^^^^^ meta.embedded.surface {#match []} # ^^ meta.brackets.elixir Value is empty @@ -59,79 +101,102 @@ {/case} # ^ punctuation.definition.tag.end.html -# ^ -punctuation.definition.tag.end.html +# ^ - punctuation.definition.tag.end.html # ^^^^^^^^^ entity.name.tag.end.surface # ^ punctuation.accessor.dot.surface # ^^^ entity.name.tag.end.surface # ^ punctuation.accessor.dot.surface # ^^^^ entity.name.tag.end.surface +#^^^^^^^^^^^^^^^^^^^^^ meta.tag.other.surface -# ^ punctuation.section.interpolation.end.elixir +# ^ punctuation.section.embedded.end.elixir # ^^^^ variable.other.constant.elixir # ^ keyword.operator.attribute.elixir -# ^ punctuation.section.interpolation.begin.elixir -# ^^^^^^^ source.elixir.interpolated.html +# ^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir # ^ punctuation.separator.key-value.html # ^^^^ entity.other.attribute-name.html -# ^ punctuation.section.interpolation.end.elixir +# ^ punctuation.section.embedded.end.elixir # ^^^^ variable.other.constant.elixir # ^ keyword.operator.attribute.elixir # ^ keyword.operator.match.elixir -# ^ punctuation.section.interpolation.begin.elixir -# ^^^^^^^^ source.elixir.interpolated.html +# ^^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir <#slot :args={value: @value, max: @max} /> -# ^^^^^^^^^^^^^^^^^^^^^^^^^^ source.elixir.interpolated.html +# ^ punctuation.section.embedded.end.elixir +# ^^^^^^^^^^^^^^^^^^^^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir +# ^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.attribute-with-value.html meta.embedded.surface - string # ^ punctuation.separator.key-value.html # ^^^^ entity.other.attribute-name.surface +# ^ punctuation.definition.attribute.begin.surface +# ^^^^^^ meta.tag.other.surface meta.attribute-with-value.html - meta.string - meta.embedded #^^^^^ entity.name.tag.begin.surface +#^^^^^^ meta.tag.other.surface - meta.attribute-with-value <:slot> # ^^^^^ entity.name.tag.end.surface #^^^^^ entity.name.tag.begin.surface +#^^^^^^^^^^^^^^ meta.tag.other.surface - meta.attribute-with-value <#Raw> # ^ punctuation.definition.tag.end.html # ^^^^ entity.name.tag.begin.surface # ^ punctuation.definition.tag.begin.html +# ^^^^^^ meta.tag.other.surface - meta.attribute-with-value <#Raw> -# ^ -punctuation.definition.tag.end.html -# ^^^^ -entity.name.tag.begin.surface -# ^ -punctuation.definition.tag.begin.html +# ^ - punctuation +# ^^^^ - entity +# ^ - punctuation <:slot args={@args}> -# ^^^^^ -entity.name -# ^^^^^^^^^^^^ -variable -entity -punctuation -# ^^^^^ -entity.name - -# ^^^^ entity.name.tag.inline.any.html -# ^^^^^^^^ -source.elixir.interpolated +# ^^^^^ - entity +# ^^^^^^^^^^^^ - variable - entity - punctuation +# ^^^^^ - entity + +# ^^^^ entity.name.tag.inline.any.html +# ^^^^^^^^ - source.elixir +# ^^^^^^^^ - source.elixir +# ^^^^^^^^ - source.elixir # ^^^^ entity.name.tag.inline.any.html # ^ punctuation.definition.tag.end.html # ^^^^ entity.name.tag.end.surface # ^^ punctuation.definition.tag.begin.html +# ^^^^^^^ meta.tag.other.surface - meta.attribute-with-value <#Markdown class="content" opts={x: "y"}> # ^ punctuation.definition.tag.end.html -# ^^^^^^^^ source.elixir.interpolated.html +# ^ meta.tag.other.surface - meta.attribute-with-value +# ^ punctuation.section.embedded.end.elixir - source +# ^^^^^^ source.elixir.embedded.html +# ^ punctuation.section.embedded.begin.elixir - source +# ^^^^^^^^^^^^^ meta.tag.other.surface meta.attribute-with-value.html +# ^ meta.tag.other.surface - meta.attribute-with-value # ^^^^^^^^^ string.quoted.double.html +# ^^^^^^^^^^^^^^^ meta.tag.other.surface meta.attribute-with-value.class.html # ^^^^^ entity.other.attribute-name.class.html # ^^^^^^^^^ entity.name.tag.begin.surface # ^ punctuation.definition.tag.begin.html +# ^^^^^^^^^^^ meta.tag.other.surface - meta.attribute-with-value # Markdown <#Markdown> -#^^^^^^^^^ -entity.name.tag.end.surface +#^^^^^^^^^ - entity.name.tag.end.surface # ^^^^^^^^^ entity.name.tag.end.surface +#^^^^^^^^^^^ meta.tag.other.surface +# ^ - meta.tag # ^^^^ entity.name.tag.inline.any.html # ^^^^ entity.name.tag.inline.any.html +# ^ - meta.tag # ^ punctuation.definition.tag.end.html # ^^^^^^^^^ entity.name.tag.end.surface # ^^ punctuation.definition.tag.begin.surface +# ^^^^^^^^^^^^ meta.tag.other.surface -

~H"<.form><%= @input %></.form>" +
~H"<.form><%= @input %></.form>"
diff --git a/images/elixir_json_example.svg b/images/elixir_json_example.svg index a959933a..cfeb0872 100644 --- a/images/elixir_json_example.svg +++ b/images/elixir_json_example.svg @@ -1,4 +1,4 @@ - + -
~L"<div><%= @live! %></div>" +
~L"<div><%= @live! %></div>"
diff --git a/images/elixir_regex_example.svg b/images/elixir_regex_example.svg index af821edb..c50be630 100644 --- a/images/elixir_regex_example.svg +++ b/images/elixir_regex_example.svg @@ -1,6 +1,6 @@ - + +## Testing + +Build-files as well as commands are provided for calling `mix test`. The predefined shortcuts can be changed via `Preferences > Package Settings > ElixirSyntax > Key Bindings`. + +Tip: To run specific tests in the current file, mark them with multiple cursors and/or spanning selections and press `Alt+Shift+T` or choose `Mix Test: Selection(s)` from the palette. + +*ElixirSyntax* stores a per-project JSON settings file in the root folder that contains both the `mix.exs` file and the `_build/` folder. They override the general settings below. + +General settings example (via `Preferences > Package Settings > ElixirSyntax > Settings`): +```json +{ + "mix_test": { + "output": "tab", + "output_mode": null, + "output_scroll_time": 2, + "args": ["--coverage"], + "seed": null + } +} +``` + +When a `mix test` command is run the first time, a `mix_test.repeat.json` file is stored in the `_build/` folder to remember the command arguments. By pressing `Alt+Shift+R` or running `Mix Test: Repeat` from the palette you can repeat the previously executed tests. + +## Formatting + +Use the default shortcut `Alt+Shift+F` or the palette command `Mix Format: File` to format your Elixir code. Format the whole project via `Mix Format: Project / Folder`. Configure auto-formatting on save via the palette command `Mix Format: Toggle Auto-Formatting` or via the menu `Preferences > Package Settings > ElixirSyntax > Settings`. There is no per-project auto-format setting yet. + +```json +{ + "mix_format": { + "on_save": true + } +} +``` + +## Palette commands + +- `ElixirSyntax: Settings` +- `Mix Test: Settings` +- `Mix Test: File` +- `Mix Test: Selection(s)` +- `Mix Test: Failed` +- `Mix Test: Repeat` +- `Mix Test: Set Seed` +- `Mix Test: Toggle --stale Flag` +- `Mix Format: File` +- `Mix Format: Project / Folder` +- `Mix Format: Toggle Auto-Formatting` + ## Recommended packages * [LSP](https://packagecontrol.io/packages/LSP) and [LSP-elixir](https://packagecontrol.io/packages/LSP-elixir) for intelligent code completion and additional snippet suggestions. diff --git a/builds/Elixir - mix test FILE.sublime-build b/builds/Elixir - mix test FILE.sublime-build index 57c68d54..5e3a6434 100644 --- a/builds/Elixir - mix test FILE.sublime-build +++ b/builds/Elixir - mix test FILE.sublime-build @@ -1,5 +1,5 @@ { - "shell_cmd": "cd \"$folder\" && mix test $file", + "target": "mix_test_file", "working_dir": "${project_path}", - "selector": "source.elixir" + "file_patterns": ["*_test.exs"] } diff --git a/builds/Elixir - mix test.sublime-build b/builds/Elixir - mix test.sublime-build index e5300bec..e935df3c 100644 --- a/builds/Elixir - mix test.sublime-build +++ b/builds/Elixir - mix test.sublime-build @@ -1,5 +1,4 @@ { - "shell_cmd": "cd \"$folder\" && mix test", - "working_dir": "${project_path}", - "selector": "source.elixir" + "target": "mix_test", + "working_dir": "${project_path}" } diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands new file mode 100644 index 00000000..91d62607 --- /dev/null +++ b/commands/Default.sublime-commands @@ -0,0 +1,16 @@ +[ + { "caption": "ElixirSyntax: Settings", "command": "edit_settings", "args": { + "base_file": "${packages}/ElixirSyntax/settings/ElixirSyntax.sublime-settings", + "default": "{\n $0\n}\n" + } }, + { "caption": "Mix Test: Settings", "command": "mix_test_settings" }, + { "caption": "Mix Test: File", "command": "mix_test_file" }, + { "caption": "Mix Test: Selection(s)", "command": "mix_test_selection" }, + { "caption": "Mix Test: Failed", "command": "mix_test_failed" }, + { "caption": "Mix Test: Repeat", "command": "mix_test_repeat" }, + { "caption": "Mix Test: Set Seed", "command": "mix_test_set_seed" }, + { "caption": "Mix Test: Toggle --stale Flag", "command": "mix_test_toggle_stale_flag" }, + { "caption": "Mix Format: File", "command": "mix_format_file" }, + { "caption": "Mix Format: Project / Folder", "command": "mix_format_project" }, + { "caption": "Mix Format: Toggle Auto-Formatting", "command": "mix_format_toggle_auto_format" }, +] diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 00000000..fdd9598a --- /dev/null +++ b/commands/__init__.py @@ -0,0 +1,2 @@ +from .mix_test import * +from .mix_format import * diff --git a/commands/mix_format.py b/commands/mix_format.py new file mode 100644 index 00000000..601b70da --- /dev/null +++ b/commands/mix_format.py @@ -0,0 +1,101 @@ +import sublime +import sublime_plugin +import subprocess +import shlex +from .utils import * + +__author__ = 'Aziz Köksal' +__email__ = 'aziz.koeksal@gmail.com' +__status__ = 'Production' + +class MixFormatProjectCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Runs `mix format` on the project path or the opened folder.' + + def run(self, **kwargs): + call_mix_format(self.window) + +class MixFormatFileCommand(sublime_plugin.TextCommand): + def description(self): + return 'Runs `mix format` on the current file.' + + def run(self, _edit, **kwargs): + window = self.view.window() + file_path = self.view.file_name() + kwargs.get('save', True) and window.run_command('save') + call_mix_format(window, file_path=file_path) + + def is_enabled(self): + return is_elixir_syntax(self.view) + +class MixFormatToggleAutoFormatCommand(sublime_plugin.TextCommand): + def description(self): + return 'Enables or disables auto-formatting on save.' + + def run(self, _edit, **kwargs): + package_settings, mix_format_settings = load_mix_format_settings() + on_save = mix_format_settings['on_save'] = not mix_format_settings.get('on_save', False) + package_settings.set('mix_format', mix_format_settings) + sublime.save_settings(SETTINGS_FILE_NAME) + print_status_msg('%s auto-formatting!' % ['Disabled', 'Enabled'][on_save]) + + def is_enabled(self): + return is_elixir_syntax(self.view) + +class MixFormatOnSaveListener(sublime_plugin.EventListener): + def is_elixir_file(self, view): + return is_elixir_syntax(view) + + def on_post_save(self, view): + if not self.is_elixir_file(view): + return + _, mix_format_settings = load_mix_format_settings() + if mix_format_settings.get('on_save', False): + MixFormatFileCommand(view).run(None, save=False) + if mix_format_settings.get('reload_after', False): + view.run_command('revert') + +def load_mix_format_settings(): + package_settings = sublime.load_settings(SETTINGS_FILE_NAME) + return (package_settings, package_settings.get('mix_format', {})) + +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 + + window_vars = window.extract_variables() + paths = file_path_list + [window_vars['project_path'], window_vars['folder']] + cwd = next((reverse_find_root_folder(p) for p in paths if p), None) + + if not (cwd or file_path): + print_status_msg( + 'Error: could not find a mix.exs file and the _build/ directory! ' + + 'Make sure that you are in a mix project and that `mix deps.get` was run.' + ) + return + + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) + stderr_data = proc.communicate()[1] + + panel_name = 'mix_format' + panel_params = {'panel': 'output.%s' % panel_name} + window.run_command('erase_view', panel_params) + + if stderr_data.strip(): + first_lines = '$ %s\n\n' % ' '.join(map(shlex.quote, cmd)) + output_view = window.create_output_panel(panel_name) + ov_settings = output_view.settings() + ov_settings.set('result_file_regex', r'/([^/]+):(\d+):(\d+)') + output_view.set_read_only(False) + output_view.run_command('append', {'characters': first_lines}) + output_view.run_command('append', {'characters': stderr_data.decode()}) + output_view.set_read_only(True) + window.run_command('show_panel', panel_params) + else: + # FIXME: closes any panel... + # window.run_command('hide_panel', panel_params) + print_status_msg( + 'Formatted %s %s!' % (file_path and 'file' or 'directory', repr(file_path or cwd)) + ) diff --git a/commands/mix_test.py b/commands/mix_test.py new file mode 100644 index 00000000..7911affc --- /dev/null +++ b/commands/mix_test.py @@ -0,0 +1,571 @@ +import sublime +import sublime_plugin +import subprocess +import shlex +import re +from os import path, fstat +from time import time as now +from datetime import datetime +from .utils import * + +__author__ = 'Aziz Köksal' +__email__ = 'aziz.koeksal@gmail.com' +__status__ = 'Production' + +# TODO: create a plugin test-suite: https://github.com/SublimeText/UnitTesting/ + +class FILE_NAMES: + SETTINGS_JSON = 'mix_test.settings.json' + REPEAT_JSON = 'mix_test.repeat.json' + +class MixTestSettingsCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Opens the `mix test` settings file for the current project.' + + def run(self, **kwargs): + abs_file_path = self.window.active_view().file_name() + window_vars = self.window.extract_variables() + mix_settings_path = reverse_find_json_path(self.window, FILE_NAMES.SETTINGS_JSON) + + if mix_settings_path: + if not path.exists(mix_settings_path): + save_json_settings(mix_settings_path, add_help_info({'args': []})) + sublime_NewFileFlags_NONE = 4 + self.window.open_file(mix_settings_path, flags=sublime_NewFileFlags_NONE) + else: + sublime.message_dialog( + '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 deps.get` has been run.' + ) + +class MixTestCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Runs the full test-suite with `mix test`.' + + def run(self, **kwargs): + call_mix_test_with_settings(self.window) + +class MixTestFileCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Runs `mix test` on the current test file.' + + def run(self, **kwargs): + abs_file_path = self.window.active_view().file_name() + assert_is_test_file(abs_file_path) + call_mix_test_with_settings(self.window, abs_file_path=abs_file_path) + + def is_enabled(self): + return is_test_file(self.window.active_view().file_name()) + +class MixTestSelectionCommand(sublime_plugin.TextCommand): + def description(self): + return 'Runs `mix test` with the currently selected lines.' + + def run(self, _edit): + abs_file_path = self.view.file_name() + assert_is_test_file(abs_file_path) + + found_test_header_regions = \ + self.view.find_all(r'(?x) ^ [^\S\n]* (describe|test) [^\S\n]* (?=[\'"] | ~[a-zA-Z])') + + found_test_header_regions = ( + r + for r in found_test_header_regions + if self.view.match_selector( + r.a + len([c for c in self.view.substr(r).rstrip() if c.isspace()]), + 'variable.function.elixir' + ) + ) + + selector_lookup_table = {} + + all_test_block_regions = ( + get_test_block_regions(self.view, header_region, selector_lookup_table) + for header_region in found_test_header_regions + ) + + all_test_block_regions = [r for r in all_test_block_regions if r] + + grouped_by_describe_dict, _ = group_by_describe_block_regions(all_test_block_regions) + + intersecting_test_regions = [ + regions + for selected_lines_region in map(self.view.line, self.view.sel()) + for regions in all_test_block_regions + if regions[-1].intersects(selected_lines_region) + ] + + grouped_isecting_by_describe_dict, test_to_describe_dict = \ + group_by_describe_block_regions(intersecting_test_regions) + + exclude_tests_set = set() + + def contains_all_tests(describe_region): + """ Returns `True` when all tests of a describe-block were selected. """ + all_tests = grouped_by_describe_dict[describe_region.to_tuple()] + contains_all = all_tests == grouped_isecting_by_describe_dict[describe_region.to_tuple()] + contains_all = contains_all and len(all_tests) > 1 + contains_all and exclude_tests_set.update(t.to_tuple() for t in all_tests) + return contains_all + + # Filter out or keep describe-block regions or their contained tests + # depending on whether all tests were selected or not. + selected_test_regions = [ + ((header_region, name_region), test_to_describe_dict.get(whole_region.to_tuple())) + for header_region, name_region, _block_region, whole_region in intersecting_test_regions + if whole_region.to_tuple() not in exclude_tests_set and ( + whole_region.to_tuple() not in grouped_isecting_by_describe_dict + or contains_all_tests(whole_region) + ) + ] + + selected_tests = [ + encode_json_test_name( + *[self.view.substr(r).strip() for r in header_and_name_regions] + + [self.view.substr(describe_tuple and describe_tuple[1] or sublime.Region(0))] + ) + for header_and_name_regions, describe_tuple in selected_test_regions + ] + + params = {'abs_file_path': abs_file_path, 'names': selected_tests} + call_mix_test_with_settings(self.view.window(), **params) + + # This function is unused but kept to have a fallback in case + # the more complicated algorithm doesn't work well enough in the wild. + def get_selected_tests_by_closest_line_nrs(self, found_test_headers, test_header_line_nrs): + """ Simpler algorithm which considers only line numbers and no do-end blocks. """ + selected_line_nrs = [self.view.rowcol(region.a)[0] + 1 for region in self.view.sel()] + test_header_line_nrs = [self.view.rowcol(region.a)[0] + 1 for region in found_test_headers] + reversed_test_header_line_nrs = list(reversed(list(enumerate(test_header_line_nrs)))) + + indexes = ( + next( + (i for (i, header_line) in reversed_test_header_line_nrs if line_nr >= header_line), + None + ) + for line_nr in selected_line_nrs + ) + + test_names = [extract_test_name(self.view, r.b) for r in found_test_headers] + return [test_names[idx] for idx in sorted(list(set(indexes))) if idx != None] + + def is_enabled(self): + return is_test_file(self.view.file_name()) + +class MixTestFailedCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Repeats only tests that failed the last time.' + + def run(self, **kwargs): + call_mix_test_with_settings(self.window, failed=True) + +class MixTestRepeatCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Repeats `mix test` with the last used parameters.' + + def run(self, **kwargs): + json_path = reverse_find_json_path(self.window, path.join('_build', FILE_NAMES.REPEAT_JSON)) + + if json_path: + call_mix_test_with_settings(self.window, **load_json_settings(json_path)) + else: + print_status_msg('Error: No tests to repeat.') + +class MixTestSetSeedCommand(sublime_plugin.TextCommand): + def description(self): + return 'Sets the seed value with which `mix test` is run.' + + def run(self, _edit, seed=None): + mix_settings_path = reverse_find_json_path(self.view.window(), FILE_NAMES.SETTINGS_JSON) + if not mix_settings_path: + return + + mix_params = load_json_settings(mix_settings_path) + seed = self.view.substr(self.view.sel()[0]) if seed is None else seed + seed = seed.strip() if type(seed) == str else seed + msg = None + + if type(seed) == int or seed == '' or type(seed) == str and seed.isdecimal(): + if seed != '': + msg = 'Mix test seed set to: %d' % int(seed) + mix_params['seed'] = int(seed) + else: + msg = 'Erased mix test seed.' + 'seed' in mix_params and mix_params.pop('seed') + + save_json_settings(mix_settings_path, add_help_info(mix_params)) + + print_status_msg(msg or 'Error: cannot set mix test seed to: %s' % repr(seed)) + + def input(self, _args): + class SeedInputHandler(sublime_plugin.TextInputHandler): + def placeholder(self): return 'Enter a number or leave empty to erase.' + def validate(self, text): return text.strip().isdecimal() or text == '' + + is_decimal = self.view.substr(self.view.sel()[0]).strip().isdecimal() + return SeedInputHandler() if not is_decimal else None + +class MixTestToggleStaleFlagCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Toggles the --stale flag.' + + def run(self, **kwargs): + mix_settings_path = reverse_find_json_path(self.window, FILE_NAMES.SETTINGS_JSON) + if not mix_settings_path: + return + mix_params = load_json_settings(mix_settings_path) + args = mix_params.get('args', []) + has_stale_flag = '--stale' in args + args = [a for a in args if a != '--stale'] if has_stale_flag else args + ['--stale'] + mix_params['args'] = args + save_json_settings(mix_settings_path, mix_params) + print_status_msg('%s mix test --stale flag!' % ['Added', 'Removed'][has_stale_flag]) + + +# Helper functions: +## + +def is_test_file(file_path): return (file_path or '').endswith('_test.exs') + +def assert_is_test_file(file_path): + if not is_test_file(file_path): + msg = 'Not an Elixir test file! Its name must end with "*_test.exs"!' + print_status_msg(msg) + raise Exception(msg) + +def extract_test_name(view, test_name_begin_point): + return view.substr(expand_scope_right(view, test_name_begin_point, 'meta.string.elixir')) + +def get_test_block_regions(view, header_region, lookup_table): + """ Returns the test's header, name, do-block and complete region, or `None` otherwise. """ + name_region = expand_scope_right(view, header_region.b, 'meta.string.elixir') + point, view_size = name_region.b, view.size() + begin_scopes_counter = 0 + + while point < view_size: + token_region = view.extract_scope(point) + token_str = view.substr(token_region) + scope_names = view.scope_name(point) + + if begin_scopes_counter == 0 and token_str in ('do', 'do:'): + meta_block_elixir_scopes = ['meta.block.elixir'] * scope_names.count('meta.block.elixir') + + meta_scopes = ' '.join(meta_block_elixir_scopes + ( + [] if token_str == 'do' else ['meta.function-call.arguments.elixir'] + )) + + block_regions = lookup_table.setdefault( + meta_scopes, + meta_scopes in lookup_table or view.find_by_selector(meta_scopes) + ) + + do_block_region = next((r for r in block_regions if r.contains(point)), None) + + if do_block_region: + complete_region = sublime.Region(header_region.a, do_block_region.b) + return (header_region, name_region, do_block_region, complete_region) + + # Keep track of opening and closing tokens in order to skip unwanted do-blocks: + is_begin_scope = '.begin.elixir' in scope_names + + if token_str not in ': % # ' and (is_begin_scope or '.end.elixir' in scope_names): + point += len(token_str) - 1 + begin_scopes_counter += 1 if is_begin_scope else -1 + + point += 1 + +def group_by_describe_block_regions(test_block_regions): + """ + Returns a dict mapping a describe-block to its tests + and a dict mapping a test to its describe-block. + """ + grouped_by_describe_dict = {} + test_to_describe_dict = {} + parent_describe = None + group_stack = [] + + def maybe_put_group_stack(): + nonlocal group_stack + if group_stack: + grouped_by_describe_dict[parent_describe[-1].to_tuple()] = group_stack + group_stack = [] + + for regions in test_block_regions: + whole_region = regions[-1] + + if parent_describe and parent_describe[-1].contains(whole_region): + test_to_describe_dict[whole_region.to_tuple()] = parent_describe + group_stack.append(whole_region) + else: + maybe_put_group_stack() + parent_describe = regions + + maybe_put_group_stack() + + return (grouped_by_describe_dict, test_to_describe_dict) + +def encode_json_test_name(type, name, describe_name): + parent_describe = '%s %s\0' % ('describe', describe_name) if describe_name else '' + return parent_describe + '%s %s' % (type, name) + +def decode_json_test_name(type_names): + parts = [type_name.split(' ', 1) for type_name in type_names.split('\0', 1)] + return parts if len(parts) == 2 else [[None, None]] + parts + +def find_lines_using_test_names(file_path, test_names): + """ Scans a text file and returns located as well as unlocated tests. """ + original_file_text, pruned_text = '', '' + with open(file_path, 'r') as file: original_file_text = file.read() + + located_tests, unlocated_tests = [], [] + + # This regex relies on the code being indented/formatted properly. + describe_block_rx = re.compile( + r'(?x) ^ ([^\S\n]*) describe [^\S\n]* (?:[\'"] | ~[a-zA-Z]) [\s\S]+?' + + r'\bdo\b (?![?!:]) [\s\S]*? \n \1 \bend\b (?![?!:])', + re.MULTILINE + ) + + for [[has_parent, parent_describe_name], [type, name]] \ + in map(decode_json_test_name, test_names): + modified_file_text = original_file_text + previous_newlines = 0 + + # Different describe-blocks may contain tests with the same name. + # Need to first skip to the parent describe-block and search from there. + if has_parent: + rx = r'(?x) ^ [^\S\n]* %s [^\S\n]* %s' % ('describe', re.escape(parent_describe_name)) + match = re.search(rx, original_file_text, re.MULTILINE) + + if match: + modified_file_text = original_file_text[match.start() : ] + previous_newlines = original_file_text[0 : match.start()].count('\n') + else: + unlocated_tests.append((parent_describe_name, type, name)) + continue + elif type == 'test': + # Avoid possibly matching a test with the same name inside a do-block. + if not pruned_text: + replacer = lambda s: '\n' * s.group().count('\n') + pruned_text = re.sub(describe_block_rx, replacer, original_file_text) + + modified_file_text = pruned_text + + rx = r'(?x) ^ [^\S\n]* %s [^\S\n]* %s' % (type, re.escape(name)) + match = re.search(rx, modified_file_text, re.MULTILINE) + + if match: + line_number = modified_file_text[0 : match.start()].count('\n') + previous_newlines + 1 + located_tests.append((type, name, line_number)) + else: + unlocated_tests.append((parent_describe_name, type, name)) + + return (located_tests, unlocated_tests) + +def reverse_find_json_path(window, json_file_path): + """ Tries to find the given JSON file by going up the folder tree + and trying different starting locations. """ + window_vars = window.extract_variables() + paths = [window.active_view().file_name(), window_vars['project_path'], window_vars['folder']] + root_dir = next((reverse_find_root_folder(p) for p in paths if p), None) + + root_dir or print_status_msg( + 'Error: could not find a mix.exs file and the _build/ directory! ' + + 'Make sure that you are in a mix project and that `mix deps.get` was run.' + ) + + 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. """ + 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_settings(path.join(build_dir, FILE_NAMES.REPEAT_JSON), params) + + mix_params = load_json_settings(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) + +def call_mix_test(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'))) + + located_tests, unlocated_tests = \ + names and find_lines_using_test_names(path.join(cwd, file_path), names) or (None, None) + + if unlocated_tests: + mix_params['unlocated_tests'] = unlocated_tests + + if file_path and located_tests: + file_path += ''.join(':%s' % l for (_t, _n, l) in located_tests) + + mix_test_pckg_settings = sublime.load_settings(SETTINGS_FILE_NAME).get('mix_test', {}) + + def get_setting(key): + return next((s.get(key) for s in [mix_params, mix_test_pckg_settings] if key in s), None) + + cmd = get_setting('cmd') + args = get_setting('args') + seed = get_setting('seed') + + seed_arg = ['--seed', str(seed)] if seed is not None else [] + file_path_arg = file_path and [file_path] or [] + 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) + ) + +def write_to_output(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: + msg = 'Error: "mix_test_output" setting is not of type string, but: %s' + print_status_msg(msg % repr(type(mix_test_output))) + elif mix_test_output == 'tab': + output_view = window.new_file() + output_view.set_scratch(True) + elif mix_test_output == 'panel': + output_view = window.create_output_panel('mix_test') + window.run_command('show_panel', {'panel': 'output.mix_test'}) + elif mix_test_output.startswith('file://'): + mode = get_setting('output_mode') or 'w' + output_path = mix_test_output[7:] + if not path.isabs(output_path): + window_vars = window.extract_variables() + output_dir = window_vars['project_path'] or window_vars['folder'] + output_path = path.join(output_dir, output_path) + try: + output_file = open(output_path, mode) + except (PermissionError, FileNotFoundError, IsADirectoryError) as e: + msg = 'Error: could not open output file %s with mode %s (%s)' + print_status_msg(msg % (repr(output_path), repr(mode), e)) + + if not (output_view or output_file): + msg = 'Error: cannot run `mix test`. No valid output setting ("mix_test_output": %s).' + print_status_msg(msg % repr(mix_test_output)) + return + + if output_view: + active_view_settings = window.active_view().settings() + # output_view.assign_syntax('Packages/X/Y.sublime-syntax') + file_path = params.get('file_path') + output_view.retarget('%s.log' % (file_path and path.join(cwd, file_path))) + 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_base_dir', cwd) + output_view.set_read_only(False) + + def write_output(txt): + if output_file: + output_file.write(txt) + else: + output_view.run_command('append', {'characters': txt, 'disable_tab_translation': True}) + + if params.get('unlocated_tests'): + write_output( + 'Error: could not find previously selected tests:\n%s\n\n' % '\n'.join( + ' %d. %s%s %s' % (i + 1, d and 'describe %s -> ' % d or '', t, name.replace('\n', '\\n')) + for i, (d, t, name) in enumerate(params.get('unlocated_tests')) + ) + + 'File: %s\n\n' % path.join(cwd, params.get('file_path')) + + 'This error occurs when a test\'s name has been changed.\n' + + 'Reselect the tests to be run or edit _build/%s to fix the name(s).\n' + % FILE_NAMES.REPEAT_JSON + ) + return + + proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + 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))) + first_lines += '\n# `%s` pid: %s' % (cmd, proc.pid) + first_lines += '\n# Timestamp: %s' % datetime.now().replace(microsecond=0) + if params.get('names'): + first_lines += '\n# Selected tests:\n%s' % '\n'.join( + '# %d. %s' % (i + 1, n.replace('\0', ' -> ').replace('\n', '\\n')) + for i, n in enumerate(params.get('names')) + ) + + print(PRINT_PREFIX + ''.join('\n' + (line and ' ' + line) for line in first_lines.split('\n'))) + write_output(first_lines + '\n\n') + + past_time = now() + + 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')) + + 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 + + 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 + ) + + if can_stop: + print_status_msg('Stopping `%s` (pid=%s).' % (cmd, proc.pid)) + proc.send_signal(subprocess.signal.SIGQUIT) + +def add_help_info(dict_data): + dict_data['help'] = { + '': 'To configure a setting add the key to the root JSON object.', + 'output': {'description': 'Choose where to display the command\'s output.', 'default': 'panel', 'values': ['tab', 'panel', 'file://...']}, + '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 + +def remove_help_info(dict_data): + 'help' in dict_data and dict_data.pop('help') + return dict_data diff --git a/commands/utils.py b/commands/utils.py new file mode 100644 index 00000000..66972ca3 --- /dev/null +++ b/commands/utils.py @@ -0,0 +1,61 @@ +import sublime +import json +from pathlib import Path + +__author__ = 'Aziz Köksal' +__email__ = 'aziz.koeksal@gmail.com' +__status__ = 'Production' + +SETTINGS_FILE_NAME = 'ElixirSyntax.sublime-settings' + +PRINT_PREFIX = 'ElixirSyntax:' + +def print_status_msg(msg): + print(PRINT_PREFIX, msg) + sublime.status_message(PRINT_PREFIX + ' ' + msg) + +def expand_scope_right(view, begin_point, scope): + end_point = next( + (pt for pt in range(begin_point, view.size()) if not view.match_selector(pt, scope)), + begin_point + ) + return sublime.Region(begin_point, end_point) + +def is_elixir_syntax(view): + return 'Elixir.sublime-syntax' in view.settings().get('syntax') + +def reverse_find_root_folder(bottom_path): + bottom_path = Path(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']): + return str(parent_path) + old_path, parent_path = parent_path, parent_path.parent + if old_path == parent_path: + break + + return None + +def save_json_settings(file_path, dict_data): + try: + with open(file_path, 'w') as file: + try: + return json.dump(dict_data, file, indent=2) + except: + print_status_msg('Error: could not save JSON to: %s' % file_path) + except: + print_status_msg('Error: could not open file: %s' % file_path) + +def load_json_settings(file_path): + try: + with open(file_path, 'r') as file: + try: + return json.load(file) + except: + print_status_msg('Error: could not load JSON from: %s' % file_path) + except: + exists = Path(file_path).exists() + exists and print_status_msg('Error: could not open file: %s' % file_path) + + return {} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 00000000..3d609030 --- /dev/null +++ b/main.py @@ -0,0 +1 @@ +from .commands import * From 16fd3717fba89dc79d086324a14acb143ec51182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 30 Aug 2022 22:20:17 +0200 Subject: [PATCH 32/98] Builds: added `elixirc` and `mix compile`. --- builds/Elixir - elixirc $file.sublime-build | 8 ++++++++ builds/Elixir - mix compile.sublime-build | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 builds/Elixir - elixirc $file.sublime-build create mode 100644 builds/Elixir - mix compile.sublime-build diff --git a/builds/Elixir - elixirc $file.sublime-build b/builds/Elixir - elixirc $file.sublime-build new file mode 100644 index 00000000..cbdeb34d --- /dev/null +++ b/builds/Elixir - elixirc $file.sublime-build @@ -0,0 +1,8 @@ +{ + "cmd": ["elixirc", "$file"], + "selector": "source.elixir", + "windows": { + "working_dir": "$file_path", + "cmd": ["elixirc.bat", "$file_name"] + } +} diff --git a/builds/Elixir - mix compile.sublime-build b/builds/Elixir - mix compile.sublime-build new file mode 100644 index 00000000..af94173e --- /dev/null +++ b/builds/Elixir - mix compile.sublime-build @@ -0,0 +1,17 @@ +{ + "cmd": ["mix", "compile"], + "working_dir": "${project_path:${folder}}", + "selector": "source.elixir", + "windows": { + "cmd": ["mix.bat", "compile"] + }, + "variants": [ + { + "name": "Dependencies", + "cmd": ["mix", "deps.compile"], + "windows": { + "cmd": ["mix.bat", "deps.compile"] + } + } + ] +} From 45fa427f718e3e8ec449552e19d989716a10776c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 21 Aug 2022 21:03:08 +0200 Subject: [PATCH 33/98] Preferences: added settings for folding. --- preferences/Fold.tmPreferences | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 preferences/Fold.tmPreferences diff --git a/preferences/Fold.tmPreferences b/preferences/Fold.tmPreferences new file mode 100644 index 00000000..ba8a03b2 --- /dev/null +++ b/preferences/Fold.tmPreferences @@ -0,0 +1,17 @@ + + + + scope + source.elixir + settings + + foldScopes + + begin + meta.doc punctuation.definition.string.begin + end + meta.doc punctuation.definition.string.end + + + + From f702440d82ece83b2ba8acf5b59bacfa8266f6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 31 Aug 2022 00:44:48 +0200 Subject: [PATCH 34/98] Menus: added main package settings menu + settings and keymaps file. --- keymaps/Default.sublime-keymap | 14 ++++++++ menus/Main.sublime-menu | 50 ++++++++++++++++++++++++++ settings/ElixirSyntax.sublime-settings | 27 ++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 keymaps/Default.sublime-keymap create mode 100644 menus/Main.sublime-menu create mode 100644 settings/ElixirSyntax.sublime-settings diff --git a/keymaps/Default.sublime-keymap b/keymaps/Default.sublime-keymap new file mode 100644 index 00000000..339a688b --- /dev/null +++ b/keymaps/Default.sublime-keymap @@ -0,0 +1,14 @@ +[ + // Runs the whole test-suite. + { "keys": ["ctrl+alt+shift+t"], "command": "mix_test" }, + // Runs the current test file. + { "keys": ["ctrl+alt+t"], "command": "mix_test_file" }, + // Runs the current test file but including only the tests marked by the selected lines. + { "keys": ["alt+shift+t"], "command": "mix_test_selection" }, + // Runs the previously executed command. + { "keys": ["alt+shift+r"], "command": "mix_test_repeat" }, + // Formats the source file. + { "keys": ["alt+shift+f"], "command": "mix_format_file", + "context": [ { "key": "selector", "operator": "equal", "operand": "source.elixir" } ] + }, +] diff --git a/menus/Main.sublime-menu b/menus/Main.sublime-menu new file mode 100644 index 00000000..b0716936 --- /dev/null +++ b/menus/Main.sublime-menu @@ -0,0 +1,50 @@ +[ + { + "id": "preferences", + "children": [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": [ + { + "caption": "ElixirSyntax", + "children": [ + { + "caption": "README", + "command": "open_file", + "args": { + "file": "${packages}/ElixirSyntax/README.md" + } + }, + { + "caption": "CHANGELOG", + "command": "open_file", + "args": { + "file": "${packages}/ElixirSyntax/CHANGELOG.md" + } + }, + { "caption": "-" }, + { + "caption": "Settings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/ElixirSyntax/settings/ElixirSyntax.sublime-settings", + "default": "{\n $0\n}\n" + } + }, + { + "caption": "Key Bindings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/ElixirSyntax/keymaps/Default.sublime-keymap", + "default": "[\n $0\n]\n" + } + } + ] + } + ] + } + ] + } +] diff --git a/settings/ElixirSyntax.sublime-settings b/settings/ElixirSyntax.sublime-settings new file mode 100644 index 00000000..9b1586bc --- /dev/null +++ b/settings/ElixirSyntax.sublime-settings @@ -0,0 +1,27 @@ +{ + "mix_test": { + // Which command to execute. + "cmd": ["mix", "test"], + // Choose where to display the command's output. + // `{"values": ["tab", "panel", "file://..."]}` + "output": "panel", + // 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": [], + // The seed with which to randomize the tests. + // `{"values": [null, "non-negative integer"]}` + "seed": null + }, + "mix_format": { + // Which command to execute. + "cmd": ["mix", "format"], + // Calls `mix format` automatically on saving the file if `true`. + "on_save": false + } +} From bd0b2a27586e0f5a67b439ceedb60a5ab2c01971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 31 Aug 2022 22:35:13 +0200 Subject: [PATCH 35/98] Completions: added Phoenix LiveView attribute bindings. --- .../Phoenix_Attributes.sublime-completions | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 completions/Phoenix_Attributes.sublime-completions diff --git a/completions/Phoenix_Attributes.sublime-completions b/completions/Phoenix_Attributes.sublime-completions new file mode 100644 index 00000000..21b1b86e --- /dev/null +++ b/completions/Phoenix_Attributes.sublime-completions @@ -0,0 +1,149 @@ +{ + "scope": "text.html.heex meta.attribute-with-value", + "completions": [ + { + "trigger": "phx-value-*", + "contents": "phx-value-${1:*}={$2}", + "kind": "snippet", + "details": "Params" + }, + { + "trigger": "phx-click", + "contents": "phx-click={$1}", + "kind": "snippet", + "details": "Click Events" + }, + { + "trigger": "phx-click-away", + "contents": "phx-click-away={$1}", + "kind": "snippet", + "details": "Click Events" + }, + { + "trigger": "phx-change", + "contents": "phx-change={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-submit", + "contents": "phx-submit={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-feedback-for", + "contents": "phx-feedback-for={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-disable-with", + "contents": "phx-disable-with={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-trigger-action", + "contents": "phx-trigger-action={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-auto-recover", + "contents": "phx-auto-recover={$1}", + "kind": "snippet", + "details": "Form Events" + }, + { + "trigger": "phx-blur", + "contents": "phx-blur={$1}", + "kind": "snippet", + "details": "Focus Events" + }, + { + "trigger": "phx-focus", + "contents": "phx-focus={$1}", + "kind": "snippet", + "details": "Focus Events" + }, + { + "trigger": "phx-window-blur", + "contents": "phx-window-blur={$1}", + "kind": "snippet", + "details": "Focus Events" + }, + { + "trigger": "phx-window-focus", + "contents": "phx-window-focus={$1}", + "kind": "snippet", + "details": "Focus Events" + }, + { + "trigger": "phx-keydown", + "contents": "phx-keydown={$1}", + "kind": "snippet", + "details": "Key Events" + }, + { + "trigger": "phx-keyup", + "contents": "phx-keyup={$1}", + "kind": "snippet", + "details": "Key Events" + }, + { + "trigger": "phx-window-keydown", + "contents": "phx-window-keydown={$1}", + "kind": "snippet", + "details": "Key Events" + }, + { + "trigger": "phx-window-keyup", + "contents": "phx-window-keyup={$1}", + "kind": "snippet", + "details": "Key Events" + }, + { + "trigger": "phx-key", + "contents": "phx-key={$1}", + "kind": "snippet", + "details": "Key Events" + }, + { + "trigger": "phx-update", + "contents": "phx-update={$1}", + "kind": "snippet", + "details": "DOM Patching" + }, + { + "trigger": "phx-remove", + "contents": "phx-remove={$1}", + "kind": "snippet", + "details": "DOM Patching" + }, + { + "trigger": "phx-hook", + "contents": "phx-hook={$1}", + "kind": "snippet", + "details": "JS Interop" + }, + { + "trigger": "phx-debounce", + "contents": "phx-debounce={$1}", + "kind": "snippet", + "details": "Rate Limiting" + }, + { + "trigger": "phx-throttle", + "contents": "phx-throttle={$1}", + "kind": "snippet", + "details": "Rate Limiting" + }, + { + "trigger": "phx-track-static", + "contents": "phx-track-static={$1}", + "kind": "snippet", + "details": "Static Tracking" + } + ] +} From 28c2529f805053d289e023602048b76bc2d02347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 18 Sep 2022 15:16:28 +0200 Subject: [PATCH 36/98] Completions: added Surface attributes. --- .../Surface_Attributes.sublime-completions | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 completions/Surface_Attributes.sublime-completions diff --git a/completions/Surface_Attributes.sublime-completions b/completions/Surface_Attributes.sublime-completions new file mode 100644 index 00000000..573692b1 --- /dev/null +++ b/completions/Surface_Attributes.sublime-completions @@ -0,0 +1,113 @@ +{ + "scope": "text.html.surface meta.attribute-with-value", + "completions": [ + { + "trigger": "if", + "contents": ":if={$1}", + "kind": "markup", + "details": ":if" + }, + { + "trigger": "hook", + "contents": ":hook={$1}", + "kind": "markup", + "details": ":hook" + }, + { + "trigger": "show", + "contents": ":show={$1}", + "kind": "markup", + "details": ":show" + }, + { + "trigger": "let", + "contents": ":let={$1}", + "kind": "markup", + "details": ":let" + }, + { + "trigger": "args", + "contents": ":args={$1}", + "kind": "markup", + "details": ":args" + }, + { + "trigger": "values", + "contents": ":values={$1}", + "kind": "markup", + "details": ":values" + }, + { + "trigger": "on-click", + "contents": ":on-click={$1}", + "kind": "markup", + "details": ":on-click" + }, + { + "trigger": "on-capture-click", + "contents": ":on-capture-click={$1}", + "kind": "markup", + "details": ":on-capture-click" + }, + { + "trigger": "on-blur", + "contents": ":on-blur={$1}", + "kind": "markup", + "details": ":on-blur" + }, + { + "trigger": "on-focus", + "contents": ":on-focus={$1}", + "kind": "markup", + "details": ":on-focus" + }, + { + "trigger": "on-change", + "contents": ":on-change={$1}", + "kind": "markup", + "details": ":on-change" + }, + { + "trigger": "on-submit", + "contents": ":on-submit={$1}", + "kind": "markup", + "details": ":on-submit" + }, + { + "trigger": "on-keydown", + "contents": ":on-keydown={$1}", + "kind": "markup", + "details": ":on-keydown" + }, + { + "trigger": "on-keyup", + "contents": ":on-keyup={$1}", + "kind": "markup", + "details": ":on-keyup" + }, + { + "trigger": "on-window-focus", + "contents": ":on-window-focus={$1}", + "kind": "markup", + "details": ":on-window-focus" + }, + { + "trigger": "on-window-blur", + "contents": ":on-window-blur={$1}", + "kind": "markup", + "details": ":on-window-blur" + }, + { + "trigger": "on-window-keydown", + "contents": ":on-window-keydown={$1}", + "kind": "markup", + "details": ":on-window-keydown" + }, + { + "trigger": "on-window-keyup", + "contents": ":on-window-keyup={$1}", + "kind": "markup", + "details": ":on-window-keyup" + } + ] +} From 1ce91b7b5913f5b22dc4bdf014c9a8ed31bfef78 Mon Sep 17 00:00:00 2001 From: Tim Masliuchenko Date: Sat, 24 Sep 2022 23:04:31 +0100 Subject: [PATCH 37/98] Preferences: added increase / decrease indent settings (#46) --- preferences/Indentation_Rules.tmPreferences | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 preferences/Indentation_Rules.tmPreferences diff --git a/preferences/Indentation_Rules.tmPreferences b/preferences/Indentation_Rules.tmPreferences new file mode 100644 index 00000000..19ccf73b --- /dev/null +++ b/preferences/Indentation_Rules.tmPreferences @@ -0,0 +1,14 @@ + + + + scope + source.elixir + settings + + increaseIndentPattern + ^\s*(after|else|catch|rescue|^.*\b(do|<-|->|[{\[(]))\b(?![?!:])\s*$ + decreaseIndentPattern + ^\s*([}\])]\s*$|(after|else|catch|rescue|end)\b(?![?!:])) + + + From 0388f18c9ddb4bc4b8e92ab4092ccea0efb25026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 9 Aug 2022 21:31:51 +0200 Subject: [PATCH 38/98] CHANGELOG: releasing v3.0.0 --- CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ba39e6..625229d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,49 @@ # Changelog +## [v3.0.0] – 2022-10-24 + +- Elixir: removed Markdown highlighting from doc comments due to unfixable issues. +- Elixir: properly highlight arrow clauses in `for`-statement arguments. +- Elixir: match macro and record calls inside parameters and arrow clauses (`for module(module: module) <- all_modules`). +- Elixir: fixed stepped ranges as parameters (`first..last//step`). +- Elixir: fixed string interpolations clearing all scopes (`"2^8 = #{2 ** 8}"`). +- Commands: added Python code to be able to call `mix test` in various ways. +- Commands: added `mix_format_project` and `mix_format_file` commands with auto-format setting. +- Palette: added `Mix Test: ...` and `Mix Format: ...` commands. +- EEx: added syntax file for EEx in plain text files. +- HTML (EEx), Elixir (EEx): added `<%!-- ... --%>` multi-line comments. +- HTML (EEx): match EEx tags as tag and attribute names (`="value"/>`). +- HTML (HEEx): fixed matching function names in tags. +- HTML (HEEx): match phx binding attributes. +- Elixir (EEx): fixed matching comments (`<%# ... %>`). +- SQL: fixed matching decimal numbers. +- SQL: fixed matching quoted member ids (`a_table."a column"`). +- Snippets: added `dbg` keyword for `|> dbg()`. +- Snippets: added EEx tags. +- Snippets: added Elixir `#{...}` string interpolation. +- Snippets: added `require IEx; IEx.pry()` string interpolation. +- Completions: added Phoenix LiveView attribute bindings. +- Completions: added Surface tag attributes. +- Preferences: added increase / decrease indentation settings (thanks to @timfjord). +- Builds: added `elixirc` and `mix compile`. +- Menus: added "ElixirSyntax" to "Preferences > Package Settings". + ## [v2.3.0] – 2021-12-17 - - Syntaxes: refactored Surface/HEEx/EEx with many improvements (thanks to @deathaxe). - - Themes: slightly darken the embed punctuation markers for Surface and (H)EEx tags. - - Elixir: allow digits in sigil string modifiers. - - Preferences: index Elixir `@attribute` definitions for "Goto Definition". + +- Syntaxes: refactored Surface/HEEx/EEx with many improvements (thanks to @deathaxe). +- Themes: slightly darken the embed punctuation markers for Surface and (H)EEx tags. +- Elixir: allow digits in sigil string modifiers. +- Preferences: index Elixir `@attribute` definitions for "Goto Definition". ## [v2.2.0] – 2021-09-18 - - Syntax: added support for the [HEEx](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#sigil_H/2) template syntax inside the `~H` sigil. - - Syntax: added support for the [Surface](https://surface-ui.org/template_syntax) template syntax inside the `~F` sigil. - - Elixir: match the `**` power operator. - - HTML (EEx): switched to version 2 and removed usage of `with_prototype`. - - SQL: match the `;` token; fixed the `/**/` comment scope. - - Themes: highlight interpolated Elixir with a lighter background. - - Themes: don't italicize the sigil type. + +- Syntax: added support for the [HEEx](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#sigil_H/2) template syntax inside the `~H` sigil. +- Syntax: added support for the [Surface](https://surface-ui.org/template_syntax) template syntax inside the `~F` sigil. +- Elixir: match the `**` power operator. +- HTML (EEx): switched to version 2 and removed usage of `with_prototype`. +- SQL: match the `;` token; fixed the `/**/` comment scope. +- Themes: highlight interpolated Elixir with a lighter background. +- Themes: don't italicize the sigil type. ## [v2.1.0] – 2021-07-25 From fb51bdfa4e7e4f2531ecf25ad863de650c32a2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 29 Oct 2022 15:01:57 +0200 Subject: [PATCH 39/98] Commands: refactored / improved some code. - Renamed command caption to `Mix Test: Set --seed` - Added palette command `Mix Test: All` - Renamed unused `kwargs` -> `_kwargs` - Renamed save_json_settings() -> save_json_file() - Renamed load_json_settings() -> load_json_file() - Improved exception handling in json functions --- README.md | 3 ++- commands/Default.sublime-commands | 3 ++- commands/mix_format.py | 4 ++-- commands/mix_test.py | 30 ++++++++++++++++-------------- commands/utils.py | 24 ++++++++++++------------ 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index be5317ba..0e1deb4c 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,12 @@ Use the default shortcut `Alt+Shift+F` or the palette command `Mix Format: File` - `ElixirSyntax: Settings` - `Mix Test: Settings` +- `Mix Test: All` - `Mix Test: File` - `Mix Test: Selection(s)` - `Mix Test: Failed` - `Mix Test: Repeat` -- `Mix Test: Set Seed` +- `Mix Test: Set --seed` - `Mix Test: Toggle --stale Flag` - `Mix Format: File` - `Mix Format: Project / Folder` diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands index 91d62607..304afef8 100644 --- a/commands/Default.sublime-commands +++ b/commands/Default.sublime-commands @@ -4,11 +4,12 @@ "default": "{\n $0\n}\n" } }, { "caption": "Mix Test: Settings", "command": "mix_test_settings" }, + { "caption": "Mix Test: All", "command": "mix_test" }, { "caption": "Mix Test: File", "command": "mix_test_file" }, { "caption": "Mix Test: Selection(s)", "command": "mix_test_selection" }, { "caption": "Mix Test: Failed", "command": "mix_test_failed" }, { "caption": "Mix Test: Repeat", "command": "mix_test_repeat" }, - { "caption": "Mix Test: Set Seed", "command": "mix_test_set_seed" }, + { "caption": "Mix Test: Set --seed", "command": "mix_test_set_seed" }, { "caption": "Mix Test: Toggle --stale Flag", "command": "mix_test_toggle_stale_flag" }, { "caption": "Mix Format: File", "command": "mix_format_file" }, { "caption": "Mix Format: Project / Folder", "command": "mix_format_project" }, diff --git a/commands/mix_format.py b/commands/mix_format.py index 601b70da..a18be6cf 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -12,7 +12,7 @@ class MixFormatProjectCommand(sublime_plugin.WindowCommand): def description(self): return 'Runs `mix format` on the project path or the opened folder.' - def run(self, **kwargs): + def run(self, **_kwargs): call_mix_format(self.window) class MixFormatFileCommand(sublime_plugin.TextCommand): @@ -32,7 +32,7 @@ class MixFormatToggleAutoFormatCommand(sublime_plugin.TextCommand): def description(self): return 'Enables or disables auto-formatting on save.' - def run(self, _edit, **kwargs): + def run(self, _edit, **_kwargs): package_settings, mix_format_settings = load_mix_format_settings() on_save = mix_format_settings['on_save'] = not mix_format_settings.get('on_save', False) package_settings.set('mix_format', mix_format_settings) diff --git a/commands/mix_test.py b/commands/mix_test.py index 7911affc..c2e7e384 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -22,14 +22,14 @@ class MixTestSettingsCommand(sublime_plugin.WindowCommand): def description(self): return 'Opens the `mix test` settings file for the current project.' - def run(self, **kwargs): + def run(self, **_kwargs): abs_file_path = self.window.active_view().file_name() window_vars = self.window.extract_variables() mix_settings_path = reverse_find_json_path(self.window, FILE_NAMES.SETTINGS_JSON) if mix_settings_path: if not path.exists(mix_settings_path): - save_json_settings(mix_settings_path, add_help_info({'args': []})) + save_json_file(mix_settings_path, add_help_info({'args': []})) sublime_NewFileFlags_NONE = 4 self.window.open_file(mix_settings_path, flags=sublime_NewFileFlags_NONE) else: @@ -42,14 +42,14 @@ class MixTestCommand(sublime_plugin.WindowCommand): def description(self): return 'Runs the full test-suite with `mix test`.' - def run(self, **kwargs): + def run(self, **_kwargs): call_mix_test_with_settings(self.window) class MixTestFileCommand(sublime_plugin.WindowCommand): def description(self): return 'Runs `mix test` on the current test file.' - def run(self, **kwargs): + def run(self, **_kwargs): abs_file_path = self.window.active_view().file_name() assert_is_test_file(abs_file_path) call_mix_test_with_settings(self.window, abs_file_path=abs_file_path) @@ -156,18 +156,18 @@ class MixTestFailedCommand(sublime_plugin.WindowCommand): def description(self): return 'Repeats only tests that failed the last time.' - def run(self, **kwargs): + def run(self, **_kwargs): call_mix_test_with_settings(self.window, failed=True) class MixTestRepeatCommand(sublime_plugin.WindowCommand): def description(self): return 'Repeats `mix test` with the last used parameters.' - def run(self, **kwargs): + def run(self, **_kwargs): json_path = reverse_find_json_path(self.window, path.join('_build', FILE_NAMES.REPEAT_JSON)) if json_path: - call_mix_test_with_settings(self.window, **load_json_settings(json_path)) + call_mix_test_with_settings(self.window, **load_json_file(json_path)) else: print_status_msg('Error: No tests to repeat.') @@ -180,7 +180,7 @@ def run(self, _edit, seed=None): if not mix_settings_path: return - mix_params = load_json_settings(mix_settings_path) + mix_params = load_json_file(mix_settings_path) seed = self.view.substr(self.view.sel()[0]) if seed is None else seed seed = seed.strip() if type(seed) == str else seed msg = None @@ -193,7 +193,7 @@ def run(self, _edit, seed=None): msg = 'Erased mix test seed.' 'seed' in mix_params and mix_params.pop('seed') - save_json_settings(mix_settings_path, add_help_info(mix_params)) + save_json_file(mix_settings_path, add_help_info(mix_params)) print_status_msg(msg or 'Error: cannot set mix test seed to: %s' % repr(seed)) @@ -209,16 +209,16 @@ class MixTestToggleStaleFlagCommand(sublime_plugin.WindowCommand): def description(self): return 'Toggles the --stale flag.' - def run(self, **kwargs): + def run(self, **_kwargs): mix_settings_path = reverse_find_json_path(self.window, FILE_NAMES.SETTINGS_JSON) if not mix_settings_path: return - mix_params = load_json_settings(mix_settings_path) + mix_params = load_json_file(mix_settings_path) args = mix_params.get('args', []) has_stale_flag = '--stale' in args args = [a for a in args if a != '--stale'] if has_stale_flag else args + ['--stale'] mix_params['args'] = args - save_json_settings(mix_settings_path, mix_params) + save_json_file(mix_settings_path, mix_params) print_status_msg('%s mix test --stale flag!' % ['Added', 'Removed'][has_stale_flag]) @@ -242,6 +242,8 @@ def get_test_block_regions(view, header_region, lookup_table): point, view_size = name_region.b, view.size() begin_scopes_counter = 0 + # TODO: use view.expand_to_scope() when available? + while point < view_size: token_region = view.extract_scope(point) token_str = view.substr(token_region) @@ -390,9 +392,9 @@ def call_mix_test_with_settings(window, **params): params.setdefault('file_path', path.relpath(params['abs_file_path'], root_dir)) del params['abs_file_path'] - save_json_settings(path.join(build_dir, FILE_NAMES.REPEAT_JSON), params) + save_json_file(path.join(build_dir, FILE_NAMES.REPEAT_JSON), params) - mix_params = load_json_settings(mix_settings_path) + 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) diff --git a/commands/utils.py b/commands/utils.py index 66972ca3..541c8120 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -37,25 +37,25 @@ def reverse_find_root_folder(bottom_path): return None -def save_json_settings(file_path, dict_data): +def save_json_file(file_path, dict_data): try: - with open(file_path, 'w') as file: + with open(str(file_path), 'w') as file: try: return json.dump(dict_data, file, indent=2) - except: - print_status_msg('Error: could not save JSON to: %s' % file_path) - except: - print_status_msg('Error: could not open file: %s' % file_path) + except BaseException as e: + print_status_msg('Error: could not save JSON to: %r\nException: %s' % (file_path, e)) + except BaseException as e: + print_status_msg('Error: could not open file: %r\nException: %s' % (file_path, e)) -def load_json_settings(file_path): +def load_json_file(file_path): try: - with open(file_path, 'r') as file: + with open(str(file_path), 'r') as file: try: return json.load(file) - except: - print_status_msg('Error: could not load JSON from: %s' % file_path) - except: + except BaseException as e: + print_status_msg('Error: could not load JSON from: %r\nException: %s' % (file_path, e)) + except BaseException as e: exists = Path(file_path).exists() - exists and print_status_msg('Error: could not open file: %s' % file_path) + exists and print_status_msg('Error: could not open file: %r\nException: %s' % (file_path, e)) return {} \ No newline at end of file From f035ac0d4879993113eb5ef87c24412289458872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 29 Oct 2022 13:36:04 +0200 Subject: [PATCH 40/98] Commands: added MixTestShowPanelCommand. --- README.md | 1 + commands/Default.sublime-commands | 1 + commands/mix_test.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e1deb4c..f4395333 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Use the default shortcut `Alt+Shift+F` or the palette command `Mix Format: File` - `Mix Test: Repeat` - `Mix Test: Set --seed` - `Mix Test: Toggle --stale Flag` +- `Mix Test: Show Panel` - `Mix Format: File` - `Mix Format: Project / Folder` - `Mix Format: Toggle Auto-Formatting` diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands index 304afef8..dea457a8 100644 --- a/commands/Default.sublime-commands +++ b/commands/Default.sublime-commands @@ -11,6 +11,7 @@ { "caption": "Mix Test: Repeat", "command": "mix_test_repeat" }, { "caption": "Mix Test: Set --seed", "command": "mix_test_set_seed" }, { "caption": "Mix Test: Toggle --stale Flag", "command": "mix_test_toggle_stale_flag" }, + { "caption": "Mix Test: Show Panel", "command": "mix_test_show_panel" }, { "caption": "Mix Format: File", "command": "mix_format_file" }, { "caption": "Mix Format: Project / Folder", "command": "mix_format_project" }, { "caption": "Mix Format: Toggle Auto-Formatting", "command": "mix_format_toggle_auto_format" }, diff --git a/commands/mix_test.py b/commands/mix_test.py index c2e7e384..f9a264dc 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -14,6 +14,8 @@ # TODO: create a plugin test-suite: https://github.com/SublimeText/UnitTesting/ +PANEL_NAME = 'output.mix_test' + class FILE_NAMES: SETTINGS_JSON = 'mix_test.settings.json' REPEAT_JSON = 'mix_test.repeat.json' @@ -221,6 +223,16 @@ def run(self, **_kwargs): save_json_file(mix_settings_path, mix_params) print_status_msg('%s mix test --stale flag!' % ['Added', 'Removed'][has_stale_flag]) +class MixTestShowPanelCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Shows the output panel if existent and hidden.' + + def run(self, **_kwargs): + self.window.run_command('show_panel', {'panel': PANEL_NAME}) + + def is_enabled(self): + return PANEL_NAME in self.window.panels() + # Helper functions: ## @@ -450,7 +462,7 @@ def write_to_output(window, cmd_args, params, cwd, get_setting): output_view.set_scratch(True) elif mix_test_output == 'panel': output_view = window.create_output_panel('mix_test') - window.run_command('show_panel', {'panel': 'output.mix_test'}) + window.run_command('show_panel', {'panel': PANEL_NAME}) elif mix_test_output.startswith('file://'): mode = get_setting('output_mode') or 'w' output_path = mix_test_output[7:] From 9a5f3b6430a1f24ae502a800b6a9e3c082945979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 29 Oct 2022 14:56:48 +0200 Subject: [PATCH 41/98] Commands: added MixTestSwitchToCodeOrTestCommand. --- README.md | 1 + commands/Default.sublime-commands | 1 + commands/mix_test.py | 42 +++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/README.md b/README.md index f4395333..646b6f26 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Use the default shortcut `Alt+Shift+F` or the palette command `Mix Format: File` - `Mix Test: Repeat` - `Mix Test: Set --seed` - `Mix Test: Toggle --stale Flag` +- `Mix Test: Switch to Code or Test` - `Mix Test: Show Panel` - `Mix Format: File` - `Mix Format: Project / Folder` diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands index dea457a8..3b720708 100644 --- a/commands/Default.sublime-commands +++ b/commands/Default.sublime-commands @@ -11,6 +11,7 @@ { "caption": "Mix Test: Repeat", "command": "mix_test_repeat" }, { "caption": "Mix Test: Set --seed", "command": "mix_test_set_seed" }, { "caption": "Mix Test: Toggle --stale Flag", "command": "mix_test_toggle_stale_flag" }, + { "caption": "Mix Test: Switch to Code or Test", "command": "mix_test_switch_to_code_or_test" }, { "caption": "Mix Test: Show Panel", "command": "mix_test_show_panel" }, { "caption": "Mix Format: File", "command": "mix_format_file" }, { "caption": "Mix Format: Project / Folder", "command": "mix_format_project" }, diff --git a/commands/mix_test.py b/commands/mix_test.py index f9a264dc..296e80f0 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -4,6 +4,7 @@ import shlex import re from os import path, fstat +from pathlib import Path from time import time as now from datetime import datetime from .utils import * @@ -223,6 +224,47 @@ def run(self, **_kwargs): save_json_file(mix_settings_path, mix_params) print_status_msg('%s mix test --stale flag!' % ['Added', 'Removed'][has_stale_flag]) +class MixTestSwitchToCodeOrTestCommand(sublime_plugin.TextCommand): + def description(self): + return 'Finds the corresponding source file of the test and vice versa if possible.' + + def run(self, _edit): + window = self.view.window() + file_path = Path(self.view.file_name()) + parts = file_path.name.rsplit('_test.exs', 1) + is_test = parts[1:] == [''] + search_names = \ + [parts[0] + ext for ext in ('.ex', '.exs')] if is_test else [file_path.stem + '_test.exs'] + + counterpart_paths = [ + (folder, p) + for folder in window.folders() + for p in Path(folder).rglob("*.ex*") + if p.name in search_names + ] + + if len(counterpart_paths) > 1: + on_select = lambda i: i >= 0 and window.open_file(str(counterpart_paths[i][1])) + + file_path_items = [ + sublime.QuickPanelItem( + trigger=str(path.relative_to(folder)), + details='Folder: %s' % folder, + kind=sublime.KIND_NAVIGATION + ) + for folder, path in counterpart_paths + ] + + window.show_quick_panel(file_path_items, on_select) + elif counterpart_paths: + window.open_file(str(counterpart_paths[0][1])) + else: + test_or_code = ['test', 'code'][is_test] + print_status_msg('Error: could not find the counterpart %s file.' % test_or_code) + + def is_enabled(self): + return is_elixir_syntax(self.view) + class MixTestShowPanelCommand(sublime_plugin.WindowCommand): def description(self): return 'Shows the output panel if existent and hidden.' From 14485d10d8d2b20b5c6c645000ddf8628521b3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 29 Oct 2022 16:05:29 +0200 Subject: [PATCH 42/98] Commands: added SearchHexPackages and OpenHexDocs. --- README.md | 2 + commands/Default.sublime-commands | 2 + commands/__init__.py | 1 + commands/hex_packages.py | 210 ++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 commands/hex_packages.py diff --git a/README.md b/README.md index 646b6f26..06e7c427 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Use the default shortcut `Alt+Shift+F` or the palette command `Mix Format: File` ## Palette commands - `ElixirSyntax: Settings` +- `ElixirSyntax: Open Hex Docs` +- `ElixirSyntax: Search Hex Packages` - `Mix Test: Settings` - `Mix Test: All` - `Mix Test: File` diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands index 3b720708..82e83954 100644 --- a/commands/Default.sublime-commands +++ b/commands/Default.sublime-commands @@ -3,6 +3,8 @@ "base_file": "${packages}/ElixirSyntax/settings/ElixirSyntax.sublime-settings", "default": "{\n $0\n}\n" } }, + { "caption": "ElixirSyntax: Open Hex Docs", "command": "open_hex_docs" }, + { "caption": "ElixirSyntax: Search Hex Packages", "command": "search_hex_packages" }, { "caption": "Mix Test: Settings", "command": "mix_test_settings" }, { "caption": "Mix Test: All", "command": "mix_test" }, { "caption": "Mix Test: File", "command": "mix_test_file" }, diff --git a/commands/__init__.py b/commands/__init__.py index fdd9598a..45648323 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,2 +1,3 @@ +from .hex_packages import * from .mix_test import * from .mix_format import * diff --git a/commands/hex_packages.py b/commands/hex_packages.py new file mode 100644 index 00000000..11c33554 --- /dev/null +++ b/commands/hex_packages.py @@ -0,0 +1,210 @@ +import sublime +import sublime_plugin +import re +import webbrowser + +from pathlib import Path +from urllib import request +from urllib.error import HTTPError +from datetime import datetime +from .utils import * + +__author__ = 'Aziz Köksal' +__email__ = 'aziz.koeksal@gmail.com' +__status__ = 'Production' + +HEXDOCS_URL = 'https://hexdocs.pm' +HEX_URL = 'https://hex.pm' +ELIXIR_CORE_APP_NAMES = ['eex', 'elixir', 'ex_unit', 'hex', 'iex', 'logger', 'mix'] +name_lastmod_rx = r'hexdocs.pm/([^/]+)/[\s\S]+?([^<]+)' +PROJECT_MAX_AGE_DAYS = 365 + +class SearchHexPackagesCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Searches hex.pm and shows the results.' + + def run(self, **kwargs): + query = (kwargs.get('query') or '').strip() + + if query: + print_status_msg('Searching hex.pm for %r' % query) + sublime.set_timeout_async(lambda: search_hex_pm(self.window, query)) + + def input(self, _args): + class QueryInputHandler(sublime_plugin.TextInputHandler): + def placeholder(self): return 'Search hex.pm' + def validate(self, text): return text.strip() != '' + + return QueryInputHandler() + +class OpenHexDocsCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Finds and opens hex documentation in the browser.' + + def run(self, **_kwargs): + cache_dir = Path(sublime.cache_path(), 'ElixirSyntax') + cache_dir.exists() or cache_dir.mkdir(parents=True) + + cached_sitemap_json_path = Path(cache_dir, 'hexdocs.sitemap.json') + + sitemap_dict = {} + sitemap_url = HEXDOCS_URL + '/sitemap.xml' + + if cached_sitemap_json_path.exists(): + sitemap_dict = load_json_file(cached_sitemap_json_path) + etag = sitemap_dict['etag'] + + def refresh_sitemap(): + try: + resp = request.urlopen(request.Request(sitemap_url, headers={'If-None-Match': etag})) + sitemap_dict = fetch_parse_and_save_sitemap(resp, cached_sitemap_json_path) + show_hexdocs_list(self.window, sitemap_dict.get('projects', [])) + except HTTPError as e: + e.code == 304 or print_status_msg('Error: %s' % e) + + sublime.set_timeout_async(refresh_sitemap) + + show_hexdocs_list(self.window, sitemap_dict.get('projects', [])) + else: + print_status_msg('Downloading %r' % sitemap_url) + + def fetch_sitemap(): + try: + resp = request.urlopen(sitemap_url) + sitemap_dict = fetch_parse_and_save_sitemap(resp, cached_sitemap_json_path) + show_hexdocs_list(self.window, sitemap_dict.get('projects', [])) + except HTTPError as e: + print_status_msg('Error: could not fetch %r (status=#%s)' % (sitemap_url, resp.code)) + + sublime.set_timeout_async(fetch_sitemap) + +def search_hex_pm(window, query, **kwargs): + """ Searches hex.pm and shows the results in a quick panel overlay. """ + page = kwargs.get('page') + page_param = page and ['page=%s' % page] or [] + query = query and ''.join('%%%x' % ord(c) if c in '#&/?' else c for c in query) + get_params = '&'.join(['search=%s' % query, 'sort=recent_downloads'] + page_param) + query_url = HEX_URL + '/packages?' + get_params + resp = request.urlopen(query_url) + results_html = resp.read().decode('utf-8') + + package_list_match = re.search(r'
([\s\S]+?)\n
', results_html) + page_match = re.search(r'
  • [\s\S]+?
  • [\s\S]+?\bpage=(\d+)', results_html) + next_page = page_match and int(page_match.group(1)) + total_count_match = re.search(r'packages of (\d+) total', results_html) + total_packages_count = total_count_match and total_count_match.group(1) + + if not package_list_match: + has_no_results = 'no-results' in results_html + + msg = [ + 'could not find div.package-list in the results HTML.', + 'no results found for %r on hex.pm!' % query + ][has_no_results] + + if has_no_results: + overlay_args = {'overlay': 'command_palette', 'command': 'search_hex_packages'} + window.run_command('show_overlay', overlay_args) + window.run_command('insert', {'characters': query}) + + print_status_msg('Error: ' + msg) + return + + package_matches = re.findall(r'''(?xi) + (.+?) [\s\S]*? + total\sdownloads:\s (.+?) [\s\S]*? + (.+?) [\s\S]*? + (.+?) [\s\S]*? +

    ([^<]*)

    + ''', + package_list_match.group(1) + ) + + previous_results = kwargs.get('previous_results') or [] + + results = previous_results + [ + {'name': m[2], 'desc': m[4], 'version': m[3], 'recent_dls': m[0], 'total_dls': m[1], + 'url': HEX_URL + '/packages/' + m[2]} + for m in package_matches + ] + + selectable_results = results + [ + {'label': 'Open search query in browser', 'url': query_url, 'desc': 'Terms: %s' % query}, + ] + ( + next_page and [{ + 'label': 'Load page %d' % next_page, + 'page': next_page, + 'desc': 'Total packages found: %s' % (total_packages_count or 'unknown') + }] or [] + ) + + def on_select(i): + if i >= 0: + result = selectable_results[i] + if result.get('page'): + print_status_msg('Loading page %d on hex.pm for %r' % (next_page, query)) + cb = lambda: search_hex_pm(window, query, page=next_page, previous_results=results) + sublime.set_timeout_async(cb) + else: + webbrowser.open_new_tab(result['url']) + + placeholder = 'Open a project in the web browser.' + selected_index = len(previous_results) if previous_results else -1 + + result_items = [ + sublime.QuickPanelItem( + trigger=result.get('label') or '%s v%s' % (result['name'], result['version']), + details=result.get('desc') or '', + annotation=result.get('recent_dls') \ + and '%s recent / %s total downloads' % (result['recent_dls'], result['total_dls']) \ + or '', + kind=result.get('recent_dls') and sublime.KIND_NAVIGATION or sublime.KIND_AMBIGUOUS + ) + for result in selectable_results + ] + + window.show_quick_panel(result_items, on_select, + placeholder=placeholder, selected_index=selected_index + ) + +def fetch_parse_and_save_sitemap(resp, cached_sitemap_json_path): + """ Fetches, parses and saves the sitemap items in a JSON file. """ + etag = next( + (value for (header, value) in resp.headers.items() if header.lower() == 'etag'), None + ) + + sitemap_xml = resp.read().decode('utf-8') + elixir_core_projects = [(name, None) for name in ELIXIR_CORE_APP_NAMES] + hexdocs_projects = re.findall(name_lastmod_rx, sitemap_xml) + young_projects, old_projects, now = [], [], datetime.now() + + for name, date in hexdocs_projects: + parsed_date = datetime.strptime(date[:10], '%Y-%m-%d') + younger_than_x_days = (now - parsed_date).days <= PROJECT_MAX_AGE_DAYS + (young_projects if younger_than_x_days else old_projects).append((name, date)) + + projects = sorted(young_projects + elixir_core_projects) + old_projects + projects = [{'name': name, 'lastmod': lastmod} for (name, lastmod) in projects] + sitemap_dict = {'projects': projects, 'etag': etag} + save_json_file(cached_sitemap_json_path, sitemap_dict) + + return sitemap_dict + +def show_hexdocs_list(window, projects): + """ Shows the hexdocs projects in a quick panel overlay. """ + project_items = [ + sublime.QuickPanelItem( + trigger=project['name'], + details=project['lastmod'] \ + and 'Last modified: %s' % project['lastmod'][:-4].replace('T', ' ') \ + or '', + kind=sublime.KIND_NAVIGATION + ) + for project in projects + ] + + def on_select(i): + i >= 0 and webbrowser.open_new_tab(HEXDOCS_URL + '/' + projects[i]['name']) + + placeholder = 'Open a project\'s documentation in the web browser.' + window.show_quick_panel(project_items, on_select, placeholder=placeholder) From d23f2c14e49162cabdde4d2ccb1a31cce0c2811c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 2 Nov 2022 20:15:35 +0100 Subject: [PATCH 43/98] CHANGELOG: releasing v3.1.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625229d1..46d16f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [v3.1.0] – 2022-11-03 + +- Commands: added `mix_test_show_panel`, `mix_test_switch_to_code_or_test`, `search_hex_packages` and `open_hex_docs`. + + `Mix Test: Show Panel` reopens the test output panel if closed. + + `Mix Test: Switch to Code or Test` jumps to the corresponding code file of a test and vice versa. + + `ElixirSyntax: Open Hex Docs` displays a filterable list of all available projects on hexdocs.pm. + + `ElixirSyntax: Search Hex Packages` searches for packages on hex.pm and displays the results in a list. +- Palette: added `Mix Test: All`. +- Palette: renamed caption `Mix Test: Set Seed` to `Mix Test: Set --seed`. + ## [v3.0.0] – 2022-10-24 - Elixir: removed Markdown highlighting from doc comments due to unfixable issues. From e3499ff3606e7ba9606bbb980319d118ad676025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 8 Nov 2022 21:33:23 +0100 Subject: [PATCH 44/98] Commands: fixed mix test and format in non-project windows. Use window.folders() instead of extract_variables(). --- commands/mix_format.py | 3 +-- commands/mix_test.py | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index a18be6cf..8ab37818 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -65,8 +65,7 @@ def call_mix_format(window, **kwargs): _, cmd_setting = load_mix_format_settings() cmd = (cmd_setting.get('cmd') or ['mix', 'format']) + file_path_list - window_vars = window.extract_variables() - paths = file_path_list + [window_vars['project_path'], window_vars['folder']] + paths = file_path_list + window.folders() cwd = next((reverse_find_root_folder(p) for p in paths if p), None) if not (cwd or file_path): diff --git a/commands/mix_test.py b/commands/mix_test.py index 296e80f0..3feb2551 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -27,7 +27,6 @@ def description(self): def run(self, **_kwargs): abs_file_path = self.window.active_view().file_name() - window_vars = self.window.extract_variables() mix_settings_path = reverse_find_json_path(self.window, FILE_NAMES.SETTINGS_JSON) if mix_settings_path: @@ -198,7 +197,7 @@ def run(self, _edit, seed=None): save_json_file(mix_settings_path, add_help_info(mix_params)) - print_status_msg(msg or 'Error: cannot set mix test seed to: %s' % repr(seed)) + print_status_msg(msg or 'Error: cannot set mix test seed to: %r' % seed) def input(self, _args): class SeedInputHandler(sublime_plugin.TextInputHandler): @@ -421,8 +420,7 @@ def find_lines_using_test_names(file_path, test_names): def reverse_find_json_path(window, json_file_path): """ Tries to find the given JSON file by going up the folder tree and trying different starting locations. """ - window_vars = window.extract_variables() - paths = [window.active_view().file_name(), window_vars['project_path'], window_vars['folder']] + 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( @@ -497,8 +495,8 @@ def write_to_output(window, cmd_args, params, cwd, get_setting): output_view = output_file = None if type(mix_test_output) != str: - msg = 'Error: "mix_test_output" setting is not of type string, but: %s' - print_status_msg(msg % repr(type(mix_test_output))) + msg = 'Error: "output" setting is not of type string, but: %r' + print_status_msg(msg % type(mix_test_output)) elif mix_test_output == 'tab': output_view = window.new_file() output_view.set_scratch(True) @@ -507,20 +505,21 @@ def write_to_output(window, cmd_args, params, cwd, get_setting): window.run_command('show_panel', {'panel': PANEL_NAME}) elif mix_test_output.startswith('file://'): mode = get_setting('output_mode') or 'w' - output_path = mix_test_output[7:] - if not path.isabs(output_path): - window_vars = window.extract_variables() - output_dir = window_vars['project_path'] or window_vars['folder'] - output_path = path.join(output_dir, output_path) + output_path = Path(mix_test_output[len('file://'):]) + output_path = str( + output_path.is_absolute() and output_path + or (window.folders() + [cwd])[0] / output_path + ) + try: output_file = open(output_path, mode) except (PermissionError, FileNotFoundError, IsADirectoryError) as e: - msg = 'Error: could not open output file %s with mode %s (%s)' - print_status_msg(msg % (repr(output_path), repr(mode), e)) + msg = 'Error: could not open output file %r with mode %r (%s)' + print_status_msg(msg % (output_path, mode, e)) if not (output_view or output_file): - msg = 'Error: cannot run `mix test`. No valid output setting ("mix_test_output": %s).' - print_status_msg(msg % repr(mix_test_output)) + msg = 'Error: cannot run `mix test`. No valid output setting ("output": %r).' + print_status_msg(msg % mix_test_output) return if output_view: From 32879da5d75c82989320d82c032dc77dff12bdd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 8 Nov 2022 21:33:50 +0100 Subject: [PATCH 45/98] Commands: ignore special folders when searching for test/code file. Fix: find files in non-project windows as well. --- commands/mix_test.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/commands/mix_test.py b/commands/mix_test.py index 3feb2551..b694cd6b 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -234,11 +234,19 @@ def run(self, _edit): is_test = parts[1:] == [''] search_names = \ [parts[0] + ext for ext in ('.ex', '.exs')] if is_test else [file_path.stem + '_test.exs'] + ignored_folders = ['.elixir_ls', '_build', 'deps'] + + subpaths = [ + p + for folder in (window.folders() or [reverse_find_root_folder(file_path)]) if folder + for p in Path(folder).iterdir() + if p.is_file() or p.name not in ignored_folders + ] counterpart_paths = [ - (folder, p) - for folder in window.folders() - for p in Path(folder).rglob("*.ex*") + (subpath, p) + for subpath in subpaths + for p in (subpath.rglob("*.ex*") if subpath.is_dir() else [subpath]) if p.name in search_names ] From c3a3f5b3bc89ff3ad4506e642b35d7faa33fbb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 8 Nov 2022 21:34:37 +0100 Subject: [PATCH 46/98] CHANGELOG: releasing v3.1.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d16f4f..55129961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v3.1.1] – 2022-11-08 + +- Commands: fixed `mix format` and `mix test` in non-project windows. +- Commands: fixed finding counterpart of a test/code file in non-project windows. +- Commands: ignore `.elixir_ls`, `_build` and `deps` folders when searching for the counterpart of a test/code file. + ## [v3.1.0] – 2022-11-03 - Commands: added `mix_test_show_panel`, `mix_test_switch_to_code_or_test`, `search_hex_packages` and `open_hex_docs`. From 66ea1ca76489b05ec1fa70d192bea2d4fa999f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 12 Dec 2022 15:54:20 +0100 Subject: [PATCH 47/98] Commands: recognize more file types for mix format (#51) --- commands/mix_format.py | 6 +++--- commands/utils.py | 10 +++++++++- keymaps/Default.sublime-keymap | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index 8ab37818..e37a29cf 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -26,7 +26,7 @@ def run(self, _edit, **kwargs): call_mix_format(window, file_path=file_path) def is_enabled(self): - return is_elixir_syntax(self.view) + return is_formattable_syntax(self.view) class MixFormatToggleAutoFormatCommand(sublime_plugin.TextCommand): def description(self): @@ -40,11 +40,11 @@ def run(self, _edit, **_kwargs): print_status_msg('%s auto-formatting!' % ['Disabled', 'Enabled'][on_save]) def is_enabled(self): - return is_elixir_syntax(self.view) + return is_formattable_syntax(self.view) class MixFormatOnSaveListener(sublime_plugin.EventListener): def is_elixir_file(self, view): - return is_elixir_syntax(view) + return is_formattable_syntax(view) def on_post_save(self, view): if not self.is_elixir_file(view): diff --git a/commands/utils.py b/commands/utils.py index 541c8120..f034ec17 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -21,8 +21,16 @@ def expand_scope_right(view, begin_point, scope): ) return sublime.Region(begin_point, end_point) +def is_one_of_syntaxes(view, syntaxes): + view_syntax_path = view.settings().get('syntax') + return any(basename + '.sublime-syntax' in view_syntax_path for basename in syntaxes) + def is_elixir_syntax(view): - return 'Elixir.sublime-syntax' in view.settings().get('syntax') + return is_one_of_syntaxes(view, ['Elixir']) + +def is_formattable_syntax(view): + syntaxes = ['Elixir', 'EEx', 'Elixir (EEx)', 'HTML (EEx)', 'HTML (HEEx)', 'HTML (Surface)'] + return is_one_of_syntaxes(view, syntaxes) def reverse_find_root_folder(bottom_path): bottom_path = Path(bottom_path) diff --git a/keymaps/Default.sublime-keymap b/keymaps/Default.sublime-keymap index 339a688b..41b9d574 100644 --- a/keymaps/Default.sublime-keymap +++ b/keymaps/Default.sublime-keymap @@ -9,6 +9,6 @@ { "keys": ["alt+shift+r"], "command": "mix_test_repeat" }, // Formats the source file. { "keys": ["alt+shift+f"], "command": "mix_format_file", - "context": [ { "key": "selector", "operator": "equal", "operand": "source.elixir" } ] + "context": [ { "key": "selector", "operator": "equal", "operand": "source.elixir | source.elixir.eex | text.eex | text.html.eex | text.html.heex | text.html.surface" } ] }, ] From f0e90ccdf7b29d621b437d27bac1da23657104b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 13 Dec 2022 11:59:00 +0100 Subject: [PATCH 48/98] Commands: display another reason for unlocated tests. --- commands/mix_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands/mix_test.py b/commands/mix_test.py index b694cd6b..08f3872f 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -556,7 +556,9 @@ def write_output(txt): for i, (d, t, name) in enumerate(params.get('unlocated_tests')) ) + 'File: %s\n\n' % path.join(cwd, params.get('file_path')) - + 'This error occurs when a test\'s name has been changed.\n' + + 'This error occurs when:\n' + + '* a test\'s name has been changed, or\n' + + '* the test file has unsaved changes.\n\n' + 'Reselect the tests to be run or edit _build/%s to fix the name(s).\n' % FILE_NAMES.REPEAT_JSON ) From 1074ff903f9b610705a4f9bfc73deea8db4b5c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 13 Dec 2022 12:01:28 +0100 Subject: [PATCH 49/98] README: mention palette commands in list of features. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06e7c427..8e8c4ffd 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,11 @@ * Full PCRE syntax highlighting: * Type highlighting: * Theme adaptations for Mariana and Monokai. +* Palette commands: `ElixirSyntax: ...`, `Mix Test: ...`, `Mix Format: ...` * Build commands: `mix format`, `mix test`, `elixir $file` * Snippets for `IO.inspect`, `tap` and `then`. -Some highlighting features are not immediately evident. Among them are: +Some syntax highlighting features are not immediately evident. Among them are: ### The `fragment` and `sql` functions From 1f3ed6aa8de6b2dd893e1b949f8d6a3ef1ebe5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 13 Dec 2022 12:05:32 +0100 Subject: [PATCH 50/98] CHANGELOG: releasing v3.1.2 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55129961..71cc7efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [v3.1.2] – 2022-12-13 + +- Commands: recognize more file types to allow running `mix format` on. +- Commands: mention possibly unsaved changes when a test wasn't found. + ## [v3.1.1] – 2022-11-08 - Commands: fixed `mix format` and `mix test` in non-project windows. From 373edf18523e49dadcca1485ca0c430d2750c94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 14 Dec 2022 11:25:38 +0100 Subject: [PATCH 51/98] Added dependencies.json to require "pathlib" (#53) --- dependencies.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 dependencies.json diff --git a/dependencies.json b/dependencies.json new file mode 100644 index 00000000..f17c48bd --- /dev/null +++ b/dependencies.json @@ -0,0 +1,7 @@ +{ + "*": { + "*": [ + "pathlib" + ] + } +} From fb20f3ccdd157a69cf541406e2ce37c71f688c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 14 Dec 2022 11:27:45 +0100 Subject: [PATCH 52/98] CHANGELOG: releasing v3.1.3 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71cc7efe..b0703bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v3.1.3] – 2022-12-15 + +- Package: fix: added `dependencies.json` to require the `pathlib` library (#53). + ## [v3.1.2] – 2022-12-13 - Commands: recognize more file types to allow running `mix format` on. From f07c195092c5182099b89e5ef36109750006193d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 21 Dec 2022 22:08:59 +0100 Subject: [PATCH 53/98] Commands: call mix format asynchronously. The view locks up briefly otherwise. --- commands/mix_format.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index e37a29cf..21f1339e 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -13,7 +13,7 @@ def description(self): return 'Runs `mix format` on the project path or the opened folder.' def run(self, **_kwargs): - call_mix_format(self.window) + call_mix_format_async(self.window) class MixFormatFileCommand(sublime_plugin.TextCommand): def description(self): @@ -23,7 +23,7 @@ def run(self, _edit, **kwargs): window = self.view.window() file_path = self.view.file_name() kwargs.get('save', True) and window.run_command('save') - call_mix_format(window, file_path=file_path) + call_mix_format_async(window, file_path=file_path) def is_enabled(self): return is_formattable_syntax(self.view) @@ -59,6 +59,11 @@ def load_mix_format_settings(): package_settings = sublime.load_settings(SETTINGS_FILE_NAME) return (package_settings, package_settings.get('mix_format', {})) +def call_mix_format_async(window, **kwargs): + file_path = kwargs.get('file_path') + print_status_msg('Formatting %s!' % (file_path and repr(file_path) or 'project')) + sublime.set_timeout_async(lambda: call_mix_format(window, **kwargs)) + def call_mix_format(window, **kwargs): file_path = kwargs.get('file_path') file_path_list = file_path and [file_path] or [] From 18166ec692156aaee669497921dfcf1070e8b03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 21 Dec 2022 22:09:49 +0100 Subject: [PATCH 54/98] Commands: mention `mix do deps.get + compile` if _build/ wasn't found. --- commands/mix_format.py | 2 +- commands/mix_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index 21f1339e..3911d3cb 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -76,7 +76,7 @@ def call_mix_format(window, **kwargs): if not (cwd or file_path): print_status_msg( 'Error: could not find a mix.exs file and the _build/ directory! ' - + 'Make sure that you are in a mix project and that `mix deps.get` was run.' + + 'Make sure that you are in a mix project and that `mix do deps.get + compile` was run.' ) return diff --git a/commands/mix_test.py b/commands/mix_test.py index 08f3872f..30e6f7d5 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -37,7 +37,7 @@ def run(self, **_kwargs): else: sublime.message_dialog( '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 deps.get` has been run.' + 'Make sure that you are in a mix project and that `mix do deps.get + compile` has been run.' ) class MixTestCommand(sublime_plugin.WindowCommand): @@ -433,7 +433,7 @@ def reverse_find_json_path(window, json_file_path): root_dir or print_status_msg( 'Error: could not find a mix.exs file and the _build/ directory! ' - + 'Make sure that you are in a mix project and that `mix deps.get` was run.' + + 'Make sure that you are in a mix project and that `mix do deps.get + compile` was run.' ) return root_dir and path.join(root_dir, json_file_path) or None From c963e5f42363b339aea9f762a33f354b9c6e916b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 21 Dec 2022 22:14:33 +0100 Subject: [PATCH 55/98] CHANGELOG: releasing v3.1.4 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0703bb9..59f4b068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v3.1.4] – 2022-12-21 + +- Commands: fix: call `mix format` asynchronously to avoid locking up the view. + ## [v3.1.3] – 2022-12-15 - Package: fix: added `dependencies.json` to require the `pathlib` library (#53). From 1c0ae58f6a883e1b954d402de588cedd8b7caa00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 4 Jan 2023 00:06:46 +0100 Subject: [PATCH 56/98] Commands: show mix_format panel on stderr output. Fixed hiding mix_format panel when error-free again. --- commands/mix_format.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index 3911d3cb..1b8b1027 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -80,26 +80,32 @@ def call_mix_format(window, **kwargs): ) return - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) - stderr_data = proc.communicate()[1] + proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) panel_name = 'mix_format' panel_params = {'panel': 'output.%s' % panel_name} window.run_command('erase_view', panel_params) + output_view = None - if stderr_data.strip(): - first_lines = '$ %s\n\n' % ' '.join(map(shlex.quote, cmd)) - output_view = window.create_output_panel(panel_name) - ov_settings = output_view.settings() - ov_settings.set('result_file_regex', r'/([^/]+):(\d+):(\d+)') - output_view.set_read_only(False) - output_view.run_command('append', {'characters': first_lines}) - output_view.run_command('append', {'characters': stderr_data.decode()}) + while proc.poll() is None: + stderr_line = proc.stderr.readline().decode(encoding='UTF-8') + + if stderr_line: + if not output_view: + first_lines = '$ cd %s && %s\n\n' % (shlex.quote(cwd), ' '.join(map(shlex.quote, cmd))) + output_view = window.create_output_panel(panel_name) + output_view.settings().set('result_file_regex', r'/([^/]+):(\d+):(\d+)') + output_view.set_read_only(False) + output_view.run_command('append', {'characters': first_lines}) + window.run_command('show_panel', panel_params) + + output_view.run_command('append', {'characters': stderr_line}) + + if output_view: output_view.set_read_only(True) - window.run_command('show_panel', panel_params) else: - # FIXME: closes any panel... - # window.run_command('hide_panel', panel_params) - print_status_msg( - 'Formatted %s %s!' % (file_path and 'file' or 'directory', repr(file_path or cwd)) - ) + if window.active_panel() == panel_params['panel']: + window.run_command('hide_panel', panel_params) + + msg = 'Formatted %s %s!' % (file_path and 'file' or 'directory', repr(file_path or cwd)) + print_status_msg(msg) From d1769613379d1209c96bd0c6430a527eef0518dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 4 Jan 2023 00:07:43 +0100 Subject: [PATCH 57/98] BACKLOG: added item about EEx tags; cleaned up others --- BACKLOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index f0818dc5..8a237fa6 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -2,8 +2,6 @@ The backlog contains tasks that are planned to be worked on or ideas that may be useful. +* EEx/HEEx: fix matching EEx tags inside `script` and `style` tags, the CSS `style=""` and the JS `on...=""` attributes. * Elixir: match do-block after function call: `func(a, b) do end`. -* Elixir: allow setting the seed used for `mix test` via a command or similar. -* Elixir: use ST4's branching feature to improve some rules. -* Elixir: try to fix the rules that don't work due to the "context sanity limit" error. -* Elixir: highlight `:atom`, `identifier`, `Module.X`, `M.func/1` in Markdown comments? +* Elixir: use ST4's branching feature to improve syntax matching rules. From d302d1eea4ed4bc554e773a1b46c2a277a4f4cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Thu, 16 Mar 2023 01:20:16 +0100 Subject: [PATCH 58/98] Elixir: allow multi-letter uppercase character sigils. --- syntaxes/Elixir.sublime-syntax | 11 +++++------ tests/syntax_test_strings.ex | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index ad72f489..b69860d4 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -978,12 +978,11 @@ contexts: - include: simple_string - include: binary_string - - match: (?x) (~[a-zA-Z])\n | ~[a-zA-Z]([^{\[<(/|"']) + - match: (?x) ~ ([a-zA-Z]++ [^{\[<(/|"'] | [a-z]{2,}) comment: catch invalid sigils first scope: meta.string.elixir storage.type.string.elixir captures: - 1: invalid.illegal.sigil-string.elixir - 2: invalid.illegal.string-delimiter.elixir + 1: invalid.illegal.sigil.elixir # Look for 'a' behind the closing delimiter. # Bracket delimiters are not matched yet: <>, {}, [] and () @@ -1132,7 +1131,7 @@ contexts: - match: (?=""") pop: 1 - - match: ~Y(?=""") + - match: ~Y(?:AML)?(?=""") comment: YAML raw scope: meta.string.elixir storage.type.string.elixir push: @@ -1182,7 +1181,7 @@ contexts: - match: (?=") pop: 1 - - match: ~J(?=") + - match: ~J(?:SON)?(?=") comment: JSON raw scope: meta.string.elixir storage.type.string.elixir push: @@ -1477,7 +1476,7 @@ contexts: - include: string_closing_round - include: escaped_or_interpolated - - match: ~[A-Z] + - match: ~[A-Z]+ comment: with sigil and without interpolation scope: meta.string.elixir storage.type.string.elixir push: diff --git a/tests/syntax_test_strings.ex b/tests/syntax_test_strings.ex index c0f9b97f..bd4d8c2f 100644 --- a/tests/syntax_test_strings.ex +++ b/tests/syntax_test_strings.ex @@ -691,3 +691,16 @@ key: "#{value}\""" # ^^ constant.character.escape.char ~S|\\\|| # ^^ constant.character.escape.char + + ~sql"" +# ^ punctuation.definition.string.end +# ^ punctuation.definition.string.begin +# ^^ string.quoted.double +# ^^^ meta.string.elixir storage.type.string.elixir invalid.illegal.sigil.elixir + + ~SQL'SELECT * FROM table' +# ^ string.quoted.other.literal.upper punctuation.definition.string.end +# ^ punctuation.definition.string.begin +# ^^^^^^^^^^^^^^^^^^^^^ string.quoted.other.literal.upper +#^^^^ storage.type.string +#^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string From 4d459ad90b5862c8f3b76ec20a4ce612e0a9ef7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 4 Apr 2023 19:32:53 +0200 Subject: [PATCH 59/98] Elixir: fix: `defmodule name do end`. --- syntaxes/Elixir.sublime-syntax | 1 + tests/syntax_test_declarations.ex | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index b69860d4..e9144c29 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -1666,6 +1666,7 @@ contexts: - include: dot_operator - include: if_non_space_or_eol_pop - include: arg_comma_and_skip_ws + - include: last_id_argument - include: core_syntax_or_if_closing_pop defrecord: diff --git a/tests/syntax_test_declarations.ex b/tests/syntax_test_declarations.ex index f71bfd29..d77c3349 100644 --- a/tests/syntax_test_declarations.ex +++ b/tests/syntax_test_declarations.ex @@ -85,8 +85,9 @@ defmodule __MODULE__.Nested do end # ^ punctuation.accessor # ^^^^^^^^^^ variable.language.special-form -defmodule nil end; defmodule raise do end -# ^^^^^ keyword.control +defmodule nil end; defmodule raise do end; defmodule name do end +# ^^^^ variable -keyword.control +# ^^^^^ variable -keyword.control # ^^^ constant.language defmodule fn end; defmodule do end # ^^^ punctuation.section.block.end From 6122ca0b72acab70d130d476a64dc62d62446afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 30 Apr 2023 20:27:13 +0200 Subject: [PATCH 60/98] CHANGELOG: releasing v3.1.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f4b068..5ae30ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v3.1.5] – 2023-04-30 + +- Elixir: recognize `name` in `defmodule name do end`. +- Commands: fix: print `mix format` error output asynchronously. +- Commands: fix: hide the `mix format` error panel when the command is successful again. + ## [v3.1.4] – 2022-12-21 - Commands: fix: call `mix format` asynchronously to avoid locking up the view. From e653b757f29496afe0c6fc2c29a64fa444c7d2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 1 May 2023 20:12:05 +0200 Subject: [PATCH 61/98] SQL: recognize `FILTER (...)` --- syntaxes/SQL (Elixir).sublime-syntax | 2 +- tests/syntax_test_sql.ex.sql | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/syntaxes/SQL (Elixir).sublime-syntax b/syntaxes/SQL (Elixir).sublime-syntax index 2957d8e2..b1c997e5 100644 --- a/syntaxes/SQL (Elixir).sublime-syntax +++ b/syntaxes/SQL (Elixir).sublime-syntax @@ -21,7 +21,7 @@ contexts: all|any|array|analy[sz]e|a?symmetric|authorization|at(?=\s+time\s+zone\b)|binary|both|by | (?<=\bat\s)time(?=\s+zone\b) | (?<=\btime\s)zone | (?<=\bdo\s)nothing | (?<=\bon\s)conflict | (?<=\bwith\s)ordinality | cast|cross|column|concurrently|collat(?:e|ion)|create|distinct|(? Date: Mon, 1 May 2023 20:10:58 +0200 Subject: [PATCH 62/98] Commands: improvements to mix_format panel. * Added create_mix_format_panel() function. * Auto-scroll output every 2 sec. * Output a timestamp. * Destroy the panel when error-free. --- commands/mix_format.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index 1b8b1027..addbe98f 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -3,6 +3,8 @@ import subprocess import shlex from .utils import * +from time import time as now +from datetime import datetime __author__ = 'Aziz Köksal' __email__ = 'aziz.koeksal@gmail.com' @@ -87,25 +89,40 @@ def call_mix_format(window, **kwargs): window.run_command('erase_view', panel_params) output_view = None + past_timestamp = now() + panel_update_interval = 2 + while proc.poll() is None: stderr_line = proc.stderr.readline().decode(encoding='UTF-8') if stderr_line: if not output_view: - first_lines = '$ cd %s && %s\n\n' % (shlex.quote(cwd), ' '.join(map(shlex.quote, cmd))) - output_view = window.create_output_panel(panel_name) - output_view.settings().set('result_file_regex', r'/([^/]+):(\d+):(\d+)') - output_view.set_read_only(False) - output_view.run_command('append', {'characters': first_lines}) + output_view = create_mix_format_panel(window, panel_name, cmd, cwd) window.run_command('show_panel', panel_params) output_view.run_command('append', {'characters': stderr_line}) + if now() - past_timestamp > panel_update_interval: + output_view.show(output_view.size()) + past_timestamp = now() + if output_view: output_view.set_read_only(True) else: if window.active_panel() == panel_params['panel']: window.run_command('hide_panel', panel_params) + window.destroy_output_panel(panel_name) msg = 'Formatted %s %s!' % (file_path and 'file' or 'directory', repr(file_path or cwd)) print_status_msg(msg) + +def create_mix_format_panel(window, panel_name, cmd_args, cwd): + first_lines = '$ cd %s && %s' % (shlex.quote(cwd), ' '.join(map(shlex.quote, cmd_args))) + 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.set_read_only(False) + output_view.run_command('append', {'characters': first_lines}) + + return output_view From 3ed6e7ffc77518057188912598014e8f5b2e9d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 2 May 2023 19:07:29 +0200 Subject: [PATCH 63/98] Commands: pipe stderr to stdout. --- commands/mix_format.py | 8 ++++---- commands/mix_test.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index addbe98f..465252e9 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -82,7 +82,7 @@ def call_mix_format(window, **kwargs): ) return - proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) panel_name = 'mix_format' panel_params = {'panel': 'output.%s' % panel_name} @@ -93,14 +93,14 @@ def call_mix_format(window, **kwargs): panel_update_interval = 2 while proc.poll() is None: - stderr_line = proc.stderr.readline().decode(encoding='UTF-8') + line = proc.stdout.readline().decode(encoding='UTF-8') - if stderr_line: + if line: if not output_view: output_view = create_mix_format_panel(window, panel_name, cmd, cwd) window.run_command('show_panel', panel_params) - output_view.run_command('append', {'characters': stderr_line}) + output_view.run_command('append', {'characters': line}) if now() - past_timestamp > panel_update_interval: output_view.show(output_view.size()) diff --git a/commands/mix_test.py b/commands/mix_test.py index 30e6f7d5..8283f90f 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -564,7 +564,7 @@ def write_output(txt): ) return - proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc = subprocess.Popen(cmd_args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if output_view: output_view.settings().set('view_id', output_view.id()) From 743fbe24663fdddedbde8966aa458bdf98bad65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 2 May 2023 21:14:29 +0200 Subject: [PATCH 64/98] Commands: fixed syntax detection for enabling/disabling commands. (#58) Use scope suffixes instead of syntax file names. --- commands/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/commands/utils.py b/commands/utils.py index f034ec17..92ccf745 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -21,16 +21,15 @@ def expand_scope_right(view, begin_point, scope): ) return sublime.Region(begin_point, end_point) -def is_one_of_syntaxes(view, syntaxes): - view_syntax_path = view.settings().get('syntax') - return any(basename + '.sublime-syntax' in view_syntax_path for basename in syntaxes) +def has_one_of_scope_suffixes(view, scope_suffixes): + view_scope_suffixes = view.scope_name(0).split(' ')[0].split('.')[1:] + return any(suffix in view_scope_suffixes for suffix in scope_suffixes) def is_elixir_syntax(view): - return is_one_of_syntaxes(view, ['Elixir']) + return has_one_of_scope_suffixes(view, ['elixir']) def is_formattable_syntax(view): - syntaxes = ['Elixir', 'EEx', 'Elixir (EEx)', 'HTML (EEx)', 'HTML (HEEx)', 'HTML (Surface)'] - return is_one_of_syntaxes(view, syntaxes) + return has_one_of_scope_suffixes(view, ['elixir', 'eex', 'heex', 'surface']) def reverse_find_root_folder(bottom_path): bottom_path = Path(bottom_path) From 15303de33561c0c96a8cd8d38030c4afb7b2e5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 2 May 2023 21:38:50 +0200 Subject: [PATCH 65/98] CHANGELOG: releasing v3.2.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae30ed5..3c1b0598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v3.2.0] – 2023-05-02 + +- Commands: improved/generalized syntax detection for enabling/disabling commands. +- Commands: fix: output both stdout/stderr when running `mix format`/`mix test`. +- Commands: auto-scroll `mix format` output when it's compiling. +- SQL: recognize `FILTER` in `array_agg(x) FILTER (...)`. + ## [v3.1.5] – 2023-04-30 - Elixir: recognize `name` in `defmodule name do end`. From 8a2cf34f8779c92e36f32d24f9514147092f6154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 23 Jun 2023 11:20:51 +0200 Subject: [PATCH 66/98] Elixir: fixed `modules_or_ids_or_calls` context Created common `member_or_call_pop` context and simplified others. --- syntaxes/Elixir.sublime-syntax | 106 +++++++++++++--------------- tests/syntax_test_function_calls.ex | 9 +++ 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index e9144c29..51cced51 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -357,17 +357,9 @@ contexts: scope: punctuation.accessor.dot.elixir set: - include: arguments_paren_param_pop - - include: atom_keyword - include: module_function_call_param_pop - - include: unquote_call_pop - include: id_or_operator_call_param_pop - - include: id_member_pop - - include: module_name_pop - - include: arguments_pop - - include: quoted_remote_call_pop - - include: quoted_member_pop - - include: tuple_call_pop - - include: if_non_space_or_eol_pop + - include: member_or_call_pop function_call_param: - match: (?=\.(?!\.)) @@ -1786,38 +1778,31 @@ contexts: module_name: - match: '{{module_name}}{{no_key_suffix}}' scope: constant.other.module.elixir - - match: (?x)(:(["'])) ((?>\\\\|\\\2|(?!\2).)*?) (\2) (?=\s*\.(?!\.)) - captures: - 1: punctuation.definition.constant.begin.elixir - 3: constant.other.module.elixir - 4: punctuation.definition.constant.end.elixir - - match: (:)({{atom_symbol}})(?=\s*\.(?!\.)) - captures: - 1: punctuation.definition.constant.begin.elixir - 2: constant.other.module.elixir + - include: atom_module_name + module_name_pop: - match: '{{module_name}}{{no_key_suffix}}' scope: constant.other.module.elixir pop: 1 module_function_call_pop: - - match: ({{module_name}})\s*(\.(?!\.)) + - match: ({{module_name}})(?=\s*\.(?!\.)) comment: always a function call after a module - captures: - 1: constant.other.module.elixir - 2: punctuation.accessor.dot.elixir - set: member_call_pop + scope: constant.other.module.elixir + set: module_member_pop - member_call_pop: - # - include: sql_or_fragment - - include: quoted_remote_call_pop - - include: quoted_member_pop - - include: unquote_call_pop - - match: '{{member}}' - scope: variable.function.elixir - set: arguments_or_pop - - include: tuple_call_pop - - include: if_non_space_or_eol_pop + module_member_pop: + - match: \.(?!\.) + scope: punctuation.accessor.dot.elixir + set: + - match: '{{member}}' + comment: always a function after module name + scope: variable.function.elixir + set: arguments_or_pop + - match: (?=\() + comment: no ".()" style arguments after module name + pop: 1 + - include: member_or_call_pop id_or_operator_call_pop: - match: ({{member}})(?=\s*\.\s*\(|{{has_arguments}}) @@ -1872,19 +1857,32 @@ contexts: captures: 1: constant.character.escape.char.elixir - modules_or_ids_or_calls: + atom_module_name: - match: (?x)(:(["'])) ((?>\\\\|\\\2|(?!\2).)*?) (\2) (?=\s*\.(?!\.)) captures: 1: punctuation.definition.constant.begin.elixir 3: constant.other.module.elixir 4: punctuation.definition.constant.end.elixir - push: member_call_pop - - match: (:)({{atom_symbol}})\s*(\.(?!\.)) + - match: (:)({{atom_symbol}})(?=\s*\.(?!\.)) captures: 1: punctuation.definition.constant.begin.elixir 2: constant.other.module.elixir - 3: punctuation.accessor.dot.elixir - push: member_call_pop + + atom_module_name_call: + - match: (?x)(:(["'])) ((?>\\\\|\\\2|(?!\2).)*?) (\2) (?=\s*\.(?!\.)) + captures: + 1: punctuation.definition.constant.begin.elixir + 3: constant.other.module.elixir + 4: punctuation.definition.constant.end.elixir + push: module_member_pop + - match: (:)({{atom_symbol}})(?=\s*\.(?!\.)) + captures: + 1: punctuation.definition.constant.begin.elixir + 2: constant.other.module.elixir + push: module_member_pop + + modules_or_ids_or_calls: + - include: atom_module_name_call - match: (?={{module_name}}|{{identifier}}) push: - include: module_function_call_pop @@ -1916,18 +1914,19 @@ contexts: dot_accessor: - match: \.(?!\.) scope: punctuation.accessor.dot.elixir - push: - - include: atom_keyword - - include: module_function_call_pop - - include: unquote_call_pop - - include: id_or_operator_call_pop - - include: id_member_pop - - include: module_name_pop - - include: arguments_pop - - include: quoted_remote_call_pop - - include: quoted_member_pop - - include: tuple_call_pop - - include: if_non_space_or_eol_pop + push: member_or_call_pop + + member_or_call_pop: + - include: module_function_call_pop + - include: unquote_call_pop + - include: id_or_operator_call_pop + - include: id_member_pop + - include: module_name_pop + - include: arguments_pop + - include: quoted_remote_call_pop + - include: quoted_member_pop + - include: tuple_call_pop + - include: if_non_space_or_eol_pop ## Module attributes @@ -2339,16 +2338,11 @@ contexts: - match: (?=\.(?!\.)) set: - include: dot_operator - - include: arguments_pop - include: capture_name_pop - - include: id_or_operator_call_pop - - include: quoted_remote_call_pop - - include: quoted_member_pop - match: (?={{member}}{{no_id_key_suffix}}) push: id_member_pop - - include: tuple_call_pop - include: if_closing_token_pop - - include: if_non_space_or_eol_pop + - include: member_or_call_pop - include: capture_name_pop - include: special_form - match: (?={{identifier}}\s*\.(?!\.|\s*\()) diff --git a/tests/syntax_test_function_calls.ex b/tests/syntax_test_function_calls.ex index cf51b69f..945b2ca6 100644 --- a/tests/syntax_test_function_calls.ex +++ b/tests/syntax_test_function_calls.ex @@ -344,6 +344,15 @@ raise"msg" # ^ punctuation.section.arguments.begin # ^^^ variable.function + :"Elixir.Kernel".in 1, [1, 2, 3] +# ^^ variable.function +# ^ punctuation.accessor.dot +# ^^^^^^^^^^^^^ constant.other.module + :"Elixir.Kernel"."@" expr +# ^^^^^ meta.function-call.arguments +# ^ variable.function +# ^^^ meta.function-call + %{} |> Kernel.|> Map.put(:key, :value) # ^ punctuation.section.arguments.end # ^ punctuation.section.arguments.begin From d1c40b0e1ef35b1aad9e2c4f78b9d57a6611c9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 23 Jun 2023 12:05:07 +0200 Subject: [PATCH 67/98] SQL: recognize `CREATE TYPE` --- syntaxes/SQL (Elixir).sublime-syntax | 2 +- tests/syntax_test_sql_fragments.ex | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/syntaxes/SQL (Elixir).sublime-syntax b/syntaxes/SQL (Elixir).sublime-syntax index b1c997e5..03470642 100644 --- a/syntaxes/SQL (Elixir).sublime-syntax +++ b/syntaxes/SQL (Elixir).sublime-syntax @@ -20,7 +20,7 @@ contexts: # Taken from https://www.postgresql.org/docs/current/sql-keywords-appendix.html all|any|array|analy[sz]e|a?symmetric|authorization|at(?=\s+time\s+zone\b)|binary|both|by | (?<=\bat\s)time(?=\s+zone\b) | (?<=\btime\s)zone | (?<=\bdo\s)nothing | (?<=\bon\s)conflict | (?<=\bwith\s)ordinality - | cast|cross|column|concurrently|collat(?:e|ion)|create|distinct|(? Date: Sat, 24 Jun 2023 21:54:43 +0200 Subject: [PATCH 68/98] CI: bumped ST version to v4143. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 54062eab..133306d9 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: 4134 - default_packages: v4134 + build: 4143 + default_packages: v4143 package_name: ElixirSyntax From f841204467f6d4555a1c255c4ff98931d9211991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 24 Jun 2023 21:46:08 +0200 Subject: [PATCH 69/98] CHANGELOG: releasing v3.2.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1b0598..ea7b66b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [v3.2.1] – 2023-06-24 + +- Elixir: fixed quoted module name function calls such as `:"Elixir.Kernel".in(1, [1])` +- SQL: recognize `CREATE TYPE` + ## [v3.2.0] – 2023-05-02 - Commands: improved/generalized syntax detection for enabling/disabling commands. From 7ca81264febfde4c1b4c587c07116cbf156665eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 25 Jun 2023 12:29:18 +0200 Subject: [PATCH 70/98] Elixir: recognize `defmacro (..) do end` --- syntaxes/Elixir.sublime-syntax | 7 +++++++ tests/syntax_test_declarations.ex | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 51cced51..a9ef8a96 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -196,6 +196,13 @@ contexts: scope: entity.name.function.elixir set: function_free_form_header_pop + - match: (\()\s*(\.\.)\s*(\)) + captures: + 1: punctuation.section.group.begin.elixr + 2: entity.name.function.elixir + 3: punctuation.section.group.end.elixr + set: function_free_form_header_pop + - include: atom_keyword - include: func_do_block_pop - include: block_or_keyword diff --git a/tests/syntax_test_declarations.ex b/tests/syntax_test_declarations.ex index d77c3349..86b501b0 100644 --- a/tests/syntax_test_declarations.ex +++ b/tests/syntax_test_declarations.ex @@ -403,6 +403,10 @@ def .. a do a end def ..(a) do a end # ^ punctuation.section.group.begin # ^^ keyword.operator.range +defmacro (..) do range(__CALLER__.context, 0, -1, 1) end +# ^ punctuation.section.group.end +# ^^ entity.name.function +# ^ punctuation.section.group.begin def ... a do a end # ^^^ entity.name.function def ...(a) do a end From 21ab1a1b92d5cbd1e344d956fcdeb415c1c94bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 28 Jun 2023 00:01:20 +0200 Subject: [PATCH 71/98] Elixir: fixed module function calls in captures. --- syntaxes/Elixir.sublime-syntax | 3 --- tests/syntax_test_misc.ex | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index a9ef8a96..4b89c5ed 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -2346,9 +2346,6 @@ contexts: set: - include: dot_operator - include: capture_name_pop - - match: (?={{member}}{{no_id_key_suffix}}) - push: id_member_pop - - include: if_closing_token_pop - include: member_or_call_pop - include: capture_name_pop - include: special_form diff --git a/tests/syntax_test_misc.ex b/tests/syntax_test_misc.ex index f41a5bbc..a4a85bc2 100644 --- a/tests/syntax_test_misc.ex +++ b/tests/syntax_test_misc.ex @@ -887,6 +887,12 @@ end[] # ^^^ variable.other.capture # ^^^^^^^ variable.other.capture + &Module.func(&1) +# ^ punctuation.section.arguments.end +# ^ punctuation.section.arguments.begin +# ^^^^ variable.function +# ^ punctuation.accessor.dot + &Module |> func(&1) # ^^^^ variable.function # ^^ keyword.operator.pipe -punctuation.accessor.arity From 7546674832c9c77b322bcae9d8e4ffef6c914739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 25 Jun 2023 12:30:39 +0200 Subject: [PATCH 72/98] Commands: added `mix_test_hide_panel` command --- commands/Default.sublime-commands | 1 + commands/mix_test.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/commands/Default.sublime-commands b/commands/Default.sublime-commands index 82e83954..0e355e41 100644 --- a/commands/Default.sublime-commands +++ b/commands/Default.sublime-commands @@ -15,6 +15,7 @@ { "caption": "Mix Test: Toggle --stale Flag", "command": "mix_test_toggle_stale_flag" }, { "caption": "Mix Test: Switch to Code or Test", "command": "mix_test_switch_to_code_or_test" }, { "caption": "Mix Test: Show Panel", "command": "mix_test_show_panel" }, + { "caption": "Mix Test: Hide Panel", "command": "mix_test_hide_panel" }, { "caption": "Mix Format: File", "command": "mix_format_file" }, { "caption": "Mix Format: Project / Folder", "command": "mix_format_project" }, { "caption": "Mix Format: Toggle Auto-Formatting", "command": "mix_format_toggle_auto_format" }, diff --git a/commands/mix_test.py b/commands/mix_test.py index 8283f90f..73efb3f1 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -280,7 +280,17 @@ def run(self, **_kwargs): self.window.run_command('show_panel', {'panel': PANEL_NAME}) def is_enabled(self): - return PANEL_NAME in self.window.panels() + return PANEL_NAME != self.window.active_panel() and PANEL_NAME in self.window.panels() + +class MixTestHidePanelCommand(sublime_plugin.WindowCommand): + def description(self): + return 'Hides the output panel if visible.' + + def run(self, **_kwargs): + self.window.run_command('hide_panel', {'panel': PANEL_NAME}) + + def is_enabled(self): + return PANEL_NAME == self.window.active_panel() # Helper functions: From c2c8e265c0412e710f49ea01105a4def4f811a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 25 Jun 2023 13:06:28 +0200 Subject: [PATCH 73/98] Commands: added COULDNT_FIND_MIX_EXS message constant --- commands/mix_format.py | 5 +---- commands/mix_test.py | 10 ++-------- commands/utils.py | 6 +++++- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/commands/mix_format.py b/commands/mix_format.py index 465252e9..2ae6a07d 100644 --- a/commands/mix_format.py +++ b/commands/mix_format.py @@ -76,10 +76,7 @@ def call_mix_format(window, **kwargs): cwd = next((reverse_find_root_folder(p) for p in paths if p), None) if not (cwd or file_path): - print_status_msg( - 'Error: could not find a mix.exs file and the _build/ directory! ' - + 'Make sure that you are in a mix project and that `mix do deps.get + compile` was run.' - ) + print_status_msg(COULDNT_FIND_MIX_EXS) return proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/commands/mix_test.py b/commands/mix_test.py index 73efb3f1..9efce451 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -35,10 +35,7 @@ def run(self, **_kwargs): sublime_NewFileFlags_NONE = 4 self.window.open_file(mix_settings_path, flags=sublime_NewFileFlags_NONE) else: - sublime.message_dialog( - '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.' - ) + sublime.message_dialog(COULDNT_FIND_MIX_EXS) class MixTestCommand(sublime_plugin.WindowCommand): def description(self): @@ -441,10 +438,7 @@ 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( - 'Error: could not find a mix.exs file and the _build/ directory! ' - + 'Make sure that you are in a mix project and that `mix do deps.get + compile` was run.' - ) + root_dir or print_status_msg(COULDNT_FIND_MIX_EXS) return root_dir and path.join(root_dir, json_file_path) or None diff --git a/commands/utils.py b/commands/utils.py index 92ccf745..de07345a 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -10,6 +10,10 @@ PRINT_PREFIX = 'ElixirSyntax:' +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.' + def print_status_msg(msg): print(PRINT_PREFIX, msg) sublime.status_message(PRINT_PREFIX + ' ' + msg) @@ -65,4 +69,4 @@ def load_json_file(file_path): exists = Path(file_path).exists() exists and print_status_msg('Error: could not open file: %r\nException: %s' % (file_path, e)) - return {} \ No newline at end of file + return {} From bcee3db5f7abc5e5ae954fefb19a71f25605e873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 28 Jun 2023 00:10:31 +0200 Subject: [PATCH 74/98] CHANGELOG: releasing v3.2.2 --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7b66b3..5ed4fc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Changelog +## [v3.2.2] – 2023-06-28 + +- Elixir: fixed module function call regression in captures (`&Map.take(&1, @fields)`). +- Elixir: recognize special macro `defmacro (..) do end`. +- Commands: added `mix_test_hide_panel` command. + ## [v3.2.1] – 2023-06-24 -- Elixir: fixed quoted module name function calls such as `:"Elixir.Kernel".in(1, [1])` -- SQL: recognize `CREATE TYPE` +- Elixir: fixed quoted module name function calls such as `:"Elixir.Kernel".in(1, [1])`. +- SQL: recognize `CREATE TYPE`. ## [v3.2.0] – 2023-05-02 From a9d5664de95410c82667088dd56c054d273ce1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 12 Aug 2023 16:04:48 +0200 Subject: [PATCH 75/98] Preferences: added various comment settings --- preferences/EEx_Comments.tmPreferences | 27 +++++++++++++++++++ ...ferences => Elixir_Comments.tmPreferences} | 3 +-- preferences/Surface_Comments.tmPreferences | 27 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 preferences/EEx_Comments.tmPreferences rename preferences/{Comments.tmPreferences => Elixir_Comments.tmPreferences} (91%) create mode 100644 preferences/Surface_Comments.tmPreferences diff --git a/preferences/EEx_Comments.tmPreferences b/preferences/EEx_Comments.tmPreferences new file mode 100644 index 00000000..f1f9d022 --- /dev/null +++ b/preferences/EEx_Comments.tmPreferences @@ -0,0 +1,27 @@ + + + + name + EEx Comments + scope + text.eex | text.html.eex | text.html.heex + settings + + shellVariables + + + name + TM_COMMENT_START + value + + + + name + TM_COMMENT_END + value + ]]> + + + + + diff --git a/preferences/Comments.tmPreferences b/preferences/Elixir_Comments.tmPreferences similarity index 91% rename from preferences/Comments.tmPreferences rename to preferences/Elixir_Comments.tmPreferences index c91a8e5d..07939830 100644 --- a/preferences/Comments.tmPreferences +++ b/preferences/Elixir_Comments.tmPreferences @@ -2,7 +2,7 @@ name - Comments + Elixir Comments scope source.elixir settings @@ -19,4 +19,3 @@ - diff --git a/preferences/Surface_Comments.tmPreferences b/preferences/Surface_Comments.tmPreferences new file mode 100644 index 00000000..36134f22 --- /dev/null +++ b/preferences/Surface_Comments.tmPreferences @@ -0,0 +1,27 @@ + + + + name + Surface Comments + scope + text.html.surface + settings + + shellVariables + + + name + TM_COMMENT_START + value + + + + name + TM_COMMENT_END + value + + + + + + From 0df58d632546836fe0fe578049bf5caa8b179a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 12 Aug 2023 20:48:38 +0200 Subject: [PATCH 76/98] Themes: fix: highlight embedded comment punctuation marks --- color-schemes/Mariana.sublime-color-scheme | 4 ++-- color-schemes/Monokai.sublime-color-scheme | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/color-schemes/Mariana.sublime-color-scheme b/color-schemes/Mariana.sublime-color-scheme index 3c2cb706..375735d4 100644 --- a/color-schemes/Mariana.sublime-color-scheme +++ b/color-schemes/Mariana.sublime-color-scheme @@ -119,8 +119,8 @@ "foreground": "color(var(white) l(- 30%))" }, { - "name": "Embedded comment punctuation", - "scope": "comment.block.eex & (punctuation.section.embedded.begin.eex | punctuation.section.embedded.end.eex)", + "name": "Surface comment punctuation", + "scope": "punctuation.definition.comment.begin.surface | punctuation.definition.comment.end.surface", "foreground": "color(var(white) l(- 30%))" }, { diff --git a/color-schemes/Monokai.sublime-color-scheme b/color-schemes/Monokai.sublime-color-scheme index 5c87bde4..3a0a0ca3 100644 --- a/color-schemes/Monokai.sublime-color-scheme +++ b/color-schemes/Monokai.sublime-color-scheme @@ -121,9 +121,9 @@ "foreground": "color(var(white) l(- 30%))" }, { - "name": "Embedded comment punctuation", - "scope": "comment.block.eex & (punctuation.section.embedded.begin.eex | punctuation.section.embedded.end.eex)", - "foreground": "color(var(doc))" + "name": "Surface comment punctuation", + "scope": "punctuation.definition.comment.begin.surface | punctuation.definition.comment.end.surface", + "foreground": "color(var(white) l(- 30%))" }, { "name": "SQL boolean", From 2552278463c4ffda149dbbf97efe735d92334c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 12 Aug 2023 20:57:52 +0200 Subject: [PATCH 77/98] Tests: changed LiveView tests to HEEx Added embedded comment tests for HEEx and Surface. --- tests/syntax_test_strings.ex | 28 ++++++++++++++++++---------- tests/syntax_test_surface.ex | 5 +++++ tests/syntax_test_template.html.heex | 11 +++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/syntax_test_strings.ex b/tests/syntax_test_strings.ex index bd4d8c2f..a44443e0 100644 --- a/tests/syntax_test_strings.ex +++ b/tests/syntax_test_strings.ex @@ -153,19 +153,27 @@ heredoc text ~s"\""m ~s'\''m ~s/\//m ~s|\||m ~s<<\>>m ~s{{\}}m ~s[[\]]m ~s((\))m -## LiveView +## HEEx -~L""''a +~H""''a # ^ -string.quoted.modifiers -~L""" +~H""" ^^^^^ meta.string.elixir text.html.eex text.html.basic \""" #^^ constant.character.escape.char.elixir """''a # ^ -string.quoted.modifiers -~L""" -
    +~H""" + <%!-- Comment --%> +# ^^ punctuation.section.embedded.end +# ^^ punctuation.definition.comment.end +# ^^^ punctuation.definition.comment.begin +# ^^ punctuation.section.embedded.begin +# ^^^^^^^^^^^^^^^^^^ meta.embedded comment.block +
    +# ^^^^^^^^ source.elixir.embedded.html +# ^^^^^^^^^^ meta.attribute-with-value.class.html meta.embedded
    <%= @deploy_step %> # ^^^^^^^^^^^ variable.other.constant @@ -175,7 +183,7 @@ heredoc text ^^text.html.basic """ -~L""" +~H"""
    <%= @var %>
    # ^^^ variable.other.constant.elixir # ^ -string @@ -191,16 +199,16 @@ heredoc text # ^ storage.type.string #^^^ punctuation.definition.string.end #^^^^ meta.string.elixir -~L"
    \"
    "m +~H"
    \"
    "m # ^ punctuation.definition.string.end # ^^ constant.character.escape.char.elixir # ^^^ entity.name.tag.block.any.html -~L'
    \'
    'm +~H'
    \'
    'm # ^ punctuation.definition.string.end # ^^ constant.character.escape.char.elixir # ^^^ entity.name.tag.block.any.html - ~L/\//m ~L|\||m ~L{\}}m ~L[\]]m ~L<\>>m ~L(\))m + ~H/\//m ~H|\||m ~H{\}}m ~H[\]]m ~H<\>>m ~H(\))m # ^ string.quoted.modifiers # ^ string.quoted.modifiers # ^ string.quoted.modifiers @@ -208,7 +216,7 @@ heredoc text # ^ string.quoted.modifiers # ^ string.quoted.modifiers -~L''' +~H''' \ ^^ text.html.basic -punctuation.separator.continuation '''m diff --git a/tests/syntax_test_surface.ex b/tests/syntax_test_surface.ex index e91418d2..7ce0676e 100644 --- a/tests/syntax_test_surface.ex +++ b/tests/syntax_test_surface.ex @@ -6,6 +6,11 @@ #^^ meta.string.elixir storage.type.string.elixir # ^^^ meta.string.elixir punctuation.definition.string.begin.elixir + {!-- Comment --} +# ^^^ punctuation.definition.comment.end.surface +# ^^^^ punctuation.definition.comment.begin.surface +# ^^^^^^^^^^^^^^^^ meta.embedded.surface comment.block.surface + # ^ punctuation.section.embedded.end.elixir - source.elixir.embedded # ^ source.elixir.embedded.html diff --git a/tests/syntax_test_template.html.heex b/tests/syntax_test_template.html.heex index b5ba8426..e4a3c045 100644 --- a/tests/syntax_test_template.html.heex +++ b/tests/syntax_test_template.html.heex @@ -18,6 +18,17 @@ + <%!-- Multi-line + + + + + comment --%> + + + + + <% # Comment %> From 148826533cc5ca56904171863cdc3a77bf038b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 12 Aug 2023 21:33:41 +0200 Subject: [PATCH 78/98] Commands: fix: filter out already selected tests --- commands/mix_test.py | 10 ++++++++-- commands/utils.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/commands/mix_test.py b/commands/mix_test.py index 9efce451..baffec29 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -85,8 +85,6 @@ def run(self, _edit): all_test_block_regions = [r for r in all_test_block_regions if r] - grouped_by_describe_dict, _ = group_by_describe_block_regions(all_test_block_regions) - intersecting_test_regions = [ regions for selected_lines_region in map(self.view.line, self.view.sel()) @@ -94,6 +92,14 @@ def run(self, _edit): if regions[-1].intersects(selected_lines_region) ] + unique_intersecting_test_tuples = \ + unique_items([tuple(r.to_tuple() for r in regions) for regions in intersecting_test_regions]) + + intersecting_test_regions = \ + [tuple(sublime.Region(*t) for t in tuples) for tuples in unique_intersecting_test_tuples] + + grouped_by_describe_dict, _ = group_by_describe_block_regions(all_test_block_regions) + grouped_isecting_by_describe_dict, test_to_describe_dict = \ group_by_describe_block_regions(intersecting_test_regions) diff --git a/commands/utils.py b/commands/utils.py index de07345a..6606e3a5 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -18,6 +18,16 @@ def print_status_msg(msg): print(PRINT_PREFIX, msg) sublime.status_message(PRINT_PREFIX + ' ' + msg) +def unique_items(items): + unique_items, seen_items = [], set() + + for item in items: + if item not in seen_items: + unique_items.append(item) + seen_items.add(item) + + return unique_items + def expand_scope_right(view, begin_point, scope): end_point = next( (pt for pt in range(begin_point, view.size()) if not view.match_selector(pt, scope)), From e3b9a4e95ba7afe832ed46d0b0f4cc85dee9d537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 12 Aug 2023 21:05:32 +0200 Subject: [PATCH 79/98] CHANGELOG: releasing v3.2.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed4fc30..0dd70c51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v3.2.3] – 2023-08-13 + +- EEx, HEEx: use `<%!-- ... --%>` when toggling comments. +- EEx, HEEx, Surface: highlight begin and end punctuation marks of comments. +- Commands: fix: filter out already selected tests when using multi-cursor selections. + ## [v3.2.2] – 2023-06-28 - Elixir: fixed module function call regression in captures (`&Map.take(&1, @fields)`). From b234648c8f928a882194f334f5b7aae1ddecf409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 27 Oct 2023 17:41:59 +0200 Subject: [PATCH 80/98] CI: bumped ST version to v4180. --- .github/workflows/main.yml | 4 ++-- tests/syntax_test_sql_fragments.ex | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/tests/syntax_test_sql_fragments.ex b/tests/syntax_test_sql_fragments.ex index 02efcea1..fe7a3edf 100644 --- a/tests/syntax_test_sql_fragments.ex +++ b/tests/syntax_test_sql_fragments.ex @@ -285,7 +285,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 +328,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 From abe2ae9590029322f6a632d7492a5112340ce079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 17 Jun 2024 01:12:26 +0200 Subject: [PATCH 81/98] Tests: changed `variable.language.wildcard.asterisk` --- tests/syntax_test_sql.ex.sql | 6 +++--- tests/syntax_test_sql_fragments.ex | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 fe7a3edf..1b38e7b3 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 @@ -162,7 +162,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 +204,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(""" From 004a456177d1f7743af915a7d3d9843e797fa10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 13 Sep 2023 00:46:06 +0200 Subject: [PATCH 82/98] Elixir: fixed pin operator and expressions in struct headers Also fixed matching module function calls in captures. Render the captured function name with italic style. --- color-schemes/Mariana.sublime-color-scheme | 3 +- color-schemes/Monokai.sublime-color-scheme | 3 +- syntaxes/Elixir.sublime-syntax | 49 ++++++++++++++++------ tests/syntax_test_function_calls.ex | 21 ++++++++++ tests/syntax_test_misc.ex | 7 ++++ 5 files changed, 69 insertions(+), 14 deletions(-) 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..580af43b 100644 --- a/color-schemes/Monokai.sublime-color-scheme +++ b/color-schemes/Monokai.sublime-color-scheme @@ -68,7 +68,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/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 4b89c5ed..2b853926 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -314,7 +314,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 @@ -375,7 +375,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 +430,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 +444,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: @@ -1816,6 +1821,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 +1911,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: @@ -2354,6 +2377,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)(?= (?> diff --git a/tests/syntax_test_function_calls.ex b/tests/syntax_test_function_calls.ex index 945b2ca6..cb4833fe 100644 --- a/tests/syntax_test_function_calls.ex +++ b/tests/syntax_test_function_calls.ex @@ -788,6 +788,27 @@ for CE.source( # ^^^^^^^^^^^^^^^^ variable.other # ^^^ constant.other.keyword +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..18f85b8c 100644 --- a/tests/syntax_test_misc.ex +++ b/tests/syntax_test_misc.ex @@ -955,6 +955,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 From 2456050e7a39990bd266ff323fc847d4471603c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Thu, 4 Jan 2024 22:44:18 +0100 Subject: [PATCH 83/98] Elixir: recognize SQL strings inside `query()` and `query_many()` --- syntaxes/Elixir.sublime-syntax | 22 ++++++++++++++ tests/syntax_test_sql_fragments.ex | 47 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 2b853926..efbab780 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -1947,6 +1947,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 @@ -2413,6 +2414,8 @@ contexts: ## SQL sql_or_fragment: + - include: query_function_call + - match: (?>(fragment)|(sql))(\() captures: 1: support.function.elixir @@ -2424,6 +2427,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 diff --git a/tests/syntax_test_sql_fragments.ex b/tests/syntax_test_sql_fragments.ex index 1b38e7b3..d6ca701f 100644 --- a/tests/syntax_test_sql_fragments.ex +++ b/tests/syntax_test_sql_fragments.ex @@ -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") From 13b5bae288e00042a96d082b144a2eefa9042cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Thu, 26 Oct 2023 15:12:51 +0200 Subject: [PATCH 84/98] Commands: fix: read `mix` output unbuffered for immediate display * Removed `output_scroll_time` setting. * Make error locations clickable and jumpable. * Use the selected lines for `mix test` if no tests were found. * Prevent running `mix` again if already in progress. * Allow continuing `mix test` in the background. --- README.md | 1 - commands/mix_format.py | 35 ++++---- commands/mix_test.py | 118 ++++++++++++++++--------- commands/utils.py | 12 +++ settings/ElixirSyntax.sublime-settings | 3 - 5 files changed, 104 insertions(+), 65 deletions(-) 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/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..a398bb73 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 @@ -450,6 +457,10 @@ def reverse_find_json_path(window, json_file_path): 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: @@ -469,12 +480,12 @@ def call_mix_test_with_settings(window, **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 +496,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 +513,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 +580,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 +606,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 +624,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 +667,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..f6100e8c 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -10,6 +10,9 @@ 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.' @@ -58,6 +61,15 @@ def reverse_find_root_folder(bottom_path): 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/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": [], From b4275ce6e4d1fde6fb6cacda6d89e1b5945328c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 5 Nov 2023 15:35:18 +0100 Subject: [PATCH 85/98] Themes: use slightly darker white for regular text --- color-schemes/Monokai.sublime-color-scheme | 1 + 1 file changed, 1 insertion(+) diff --git a/color-schemes/Monokai.sublime-color-scheme b/color-schemes/Monokai.sublime-color-scheme index 580af43b..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)" }, From 902180b2b51e6bd2e5cf0b6a61c1dec7e6de59ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sat, 9 Dec 2023 21:56:53 +0100 Subject: [PATCH 86/98] HTML (HEEx): recognize special and dynamic attributes --- syntaxes/HTML (HEEx).sublime-syntax | 29 +++++++++++++++++++++ tests/syntax_test_template.html.heex | 38 +++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 7 deletions(-) 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_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 @@ + <:col :let={user}><%= user.id %>
    + + + + + + + + + + + + + + + +
    + + + + + + +
    @@ -58,12 +82,12 @@ - - - + + + - - + + <.table rows={@users}> @@ -123,8 +147,8 @@ - - + + From 9e7314a69335e8ef5fb57e0bb65f76397deab41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 16 Jun 2024 23:30:45 +0200 Subject: [PATCH 87/98] Elixir: recognize every kind of atom words sigil (`~w"x y z"a`) --- syntaxes/Elixir.sublime-syntax | 207 ++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 68 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index efbab780..ba230ff3 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -988,29 +988,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 @@ -1431,54 +1408,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 @@ -1550,6 +1489,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 From d2cb363acbf07a29ca7105a0998f01e593b00992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 16 Jun 2024 23:40:41 +0200 Subject: [PATCH 88/98] Elixir: fix: recognize `%@module{}` and `%^@module{}` --- syntaxes/Elixir.sublime-syntax | 5 +++++ tests/syntax_test_misc.ex | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index ba230ff3..9e45e240 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -2280,6 +2280,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}}) diff --git a/tests/syntax_test_misc.ex b/tests/syntax_test_misc.ex index 18f85b8c..80c646f0 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 From c782be87a2ecb11e41644d40e1017a77ea7df9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 21 Jun 2024 14:05:16 +0200 Subject: [PATCH 89/98] Elixir: fixed capture expressions * `& 1` -> don't match as a capture argument number * `& &1.func/1` -> can be a captured remote function * `&some.thing.good/1` -> several chained members * `&@module.func/1` -> captured remote function --- syntaxes/Elixir.sublime-syntax | 17 ++++++++-- tests/syntax_test_misc.ex | 58 +++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 9e45e240..c58c0630 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -2432,10 +2432,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: @@ -2443,10 +2449,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*\()) @@ -2468,6 +2478,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 diff --git a/tests/syntax_test_misc.ex b/tests/syntax_test_misc.ex index 80c646f0..233101c4 100644 --- a/tests/syntax_test_misc.ex +++ b/tests/syntax_test_misc.ex @@ -595,12 +595,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 @@ -622,8 +622,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 @@ -651,6 +651,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 @@ -672,8 +705,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 @@ -683,6 +716,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 From 2cd9592f5cd18904d3894454332dd5fcbc49612c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 3 Jul 2024 01:07:57 +0200 Subject: [PATCH 90/98] Elixir: fix: only consider first argument in `defmodule` --- syntaxes/Elixir.sublime-syntax | 5 +++-- tests/syntax_test_declarations.ex | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index c58c0630..ef127d04 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -1740,9 +1740,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}} 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); :&&.&&&(:&, :&) From 912d929b7230b8167839306628b6df9169cf2635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Wed, 3 Jul 2024 02:31:09 +0200 Subject: [PATCH 91/98] Elixir: fixed `last_assignment_clause_pop` --- syntaxes/Elixir.sublime-syntax | 6 +++++- tests/syntax_test_function_calls.ex | 32 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index ef127d04..42385882 100644 --- a/syntaxes/Elixir.sublime-syntax +++ b/syntaxes/Elixir.sublime-syntax @@ -345,7 +345,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 @@ -2847,6 +2847,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/tests/syntax_test_function_calls.ex b/tests/syntax_test_function_calls.ex index cb4833fe..b97d753f 100644 --- a/tests/syntax_test_function_calls.ex +++ b/tests/syntax_test_function_calls.ex @@ -788,6 +788,38 @@ 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 From d23eb08f19eb4d0eacf1dbbbf8e5c6a1004bfb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 23 Aug 2024 19:39:01 +0200 Subject: [PATCH 92/98] Commands: improved finding the root mix.exs file --- commands/mix_test.py | 9 ++++++--- commands/utils.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/commands/mix_test.py b/commands/mix_test.py index a398bb73..c694491a 100644 --- a/commands/mix_test.py +++ b/commands/mix_test.py @@ -451,7 +451,9 @@ 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 @@ -467,13 +469,14 @@ def merge_mix_settings_and_params(window, params): 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) diff --git a/commands/utils.py b/commands/utils.py index f6100e8c..c9075123 100644 --- a/commands/utils.py +++ b/commands/utils.py @@ -14,8 +14,8 @@ 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) @@ -53,9 +53,17 @@ 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 From cc975fee35eecf3d05b30607075dfd434f54ca4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 30 Aug 2024 14:20:53 +0200 Subject: [PATCH 93/98] Completions: use double quotes for `phx-*` attributes --- .../Phoenix_Attributes.sublime-completions | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) 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" } From cad32fca8ac54eda0502dd34c98591eb1d03cdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 30 Aug 2024 02:57:03 +0200 Subject: [PATCH 94/98] Elixir: match arrow clauses using the branch feature --- syntaxes/Elixir.sublime-syntax | 54 ++++++++++++++++------------------ tests/syntax_test_misc.ex | 39 ++++++++++++++---------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/syntaxes/Elixir.sublime-syntax b/syntaxes/Elixir.sublime-syntax index 42385882..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 diff --git a/tests/syntax_test_misc.ex b/tests/syntax_test_misc.ex index 233101c4..ba84ad10 100644 --- a/tests/syntax_test_misc.ex +++ b/tests/syntax_test_misc.ex @@ -118,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 @@ -162,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 @@ -215,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 @@ -282,7 +291,7 @@ end -> var -> var # ^^^ variable.other # ^^ keyword.operator.arrow - # ^^^ variable.other + # ^^^ variable.parameter #^^ keyword.operator.arrow expr #^^^ variable.other @@ -1252,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 @@ -1274,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 @@ -1296,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 @@ -1318,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 @@ -1340,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 From 03c7cb0ba8ee1ac0b6f9a77d64bd4d6d943f5834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Fri, 27 Oct 2023 00:40:41 +0200 Subject: [PATCH 95/98] CHANGELOG: releasing v4.0.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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. From a3207e31cce17f5669cf2fdfb2d40eaf32188fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 6 Jan 2025 14:12:55 +0100 Subject: [PATCH 96/98] HEEx: support new embed syntax with curly braces --- syntaxes/HTML (HEEx).sublime-syntax | 1 + tests/syntax_test_template.html.heex | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/syntaxes/HTML (HEEx).sublime-syntax b/syntaxes/HTML (HEEx).sublime-syntax index 858e7d35..e70cbcfd 100644 --- a/syntaxes/HTML (HEEx).sublime-syntax +++ b/syntaxes/HTML (HEEx).sublime-syntax @@ -66,6 +66,7 @@ contexts: - match: <%= @elixir %> + { "1" <> "2" } + + {"1" <> "2"} + + + + + + + + <:col :let={user}><%= user.id %>
    From 59ebcb3dd0befa5e450273d85c8a6bbfc3d9dbf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 6 Jan 2025 14:16:50 +0100 Subject: [PATCH 97/98] CI: bumped ST to v4192 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c2302a4c..9854ee82 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: 4180 - default_packages: v4180 + build: 4192 + default_packages: v4192 package_name: ElixirSyntax From 7573343d4a9cec9d4ce7e4d6067736307570a78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Mon, 6 Jan 2025 14:14:16 +0100 Subject: [PATCH 98/98] CHANGELOG: releasing v4.1.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fbccad..84509238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v4.1.0] – 2025-03-28 + +- HEEx: support new syntax for embedding Elixir code with curly braces. + ## [v4.0.0] – 2024-09-01 - Elixir: improved matching of right-arrow clauses.