Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Shopify/theme-tools
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: a7de3d4e909f1cffa2763f8dca6a789566592aee
Choose a base ref
...
head repository: Shopify/theme-tools
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 8bceaa3421524ae660b0d207fa25bc4dfe42034c
Choose a head ref

Commits on Jan 23, 2025

  1. Support default.setting completion + hover (#717)

    aswamy authored Jan 23, 2025

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    Copy the full SHA
    9765bec View commit details

Commits on Jan 24, 2025

  1. Add types

    Add util function to read liquid doc definitions for a snippet
    
    Implement SnippetHoverProvider
    
    Connect SnippetHoverProvider with server
    
    Add changeset
    
    Provide default value for getLiquidDocDefinitionsForURI
    
    Rename SnippetHoverProvider -> RenderSnippetHoverProvider
    jamesmengo committed Jan 24, 2025
    Copy the full SHA
    931dc9b View commit details
  2. Refactor - Simplify code by moving liquid doc visiting logic to its o…

    …wn module
    
    ----
    We didn't need to use the make pattern at least at this stage, so I'm removing the code in ThemeCheckCommon and placing it in its own module.
    
    We may need to revisit this when we are writing theme checks. However, for the purpose of hover, this is where the code should live.
    jamesmengo committed Jan 24, 2025
    Copy the full SHA
    fea457f View commit details
  3. Refactor Liquid documentation types and update references to improve …

    …clarity
    
    - Renamed `GetLiquidDocDefinitionsForURI` to `GetSnippetDefinitionForURI`
    - Introduced `SnippetDefinition` type to replace `LiquidDocDefinition`, enhancing type specificity.
    jamesmengo committed Jan 24, 2025
    Copy the full SHA
    83897eb View commit details
  4. Copy the full SHA
    c85a613 View commit details
  5. Rename liquidDoc to getLiquidDoc

    jamesmengo committed Jan 24, 2025
    Copy the full SHA
    52bf118 View commit details

Commits on Jan 27, 2025

  1. [LiquidDoc] Add snippet hover support inside of {% render %} tag (#703)

    ## What are you adding in this PR?
    
    Shopify/developer-tools-team#496
    
    **1) Hover support for Liquid snippets inside of a `{% render %}` tag. Users will now see:**
    - The snippet name as a header
    - A list of parameters with their types and descriptions (if 
    documented)
    
    If there is no `{% doc %}` present in the snippet, we will just render the snippet name
    
    **2) Caching for fetching LiquidDoc definitions**
    
    ##### Example snippet documentation:
    ```liquid
    {% doc %}
      @param {String} title - The title of the product
      @param {Number} border-radius - The border radius in px
    {% enddoc %}
    ```
    
    ##### When hovering over `product-card` in `{% render 'product-card' %}`, users will see:
    ```markdown
    ### product-card
    
    **Parameters:**
    - `title`: String - The title of the product
    - `border-radius`: Number - The border radius in px
    ```
    
    Uploading Cursor - liquidDoc.ts — theme-tools.mp4…
    
    #### Hovering a snippet with docs
    ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/JdHLnhebSbtZTZO01I1e/84bb1493-2812-4eba-9943-65b2d4071f9a.png)
    
    #### Hovering a snippet without liquiddoc
    ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/JdHLnhebSbtZTZO01I1e/ad09bea5-5e73-454a-9269-0ce8a187900e.png)
    
    
    ## What's next? Any followup issues?
    
    - Consider adding validation for param types
    - Add support for return value documentation
    - Add support for example usage documentation
    
    ## What did you learn?
    
    This is my first time working with the hover API in VS code. Pretty cool to read up on!
    
    ## Before you deploy
    
    - [x] I included a minor bump `changeset`
    - [x] My feature is backward compatible
    jamesmengo authored Jan 27, 2025
    Copy the full SHA
    a42459e View commit details
  2. [LiquidDoc] Modify paramDescription to be null if empty

    jamesmengo committed Jan 27, 2025
    Copy the full SHA
    dd0cd4d View commit details
  3. Copy the full SHA
    c76cae5 View commit details
  4. Copy the full SHA
    1ccf7d7 View commit details
  5. Changeset

    jamesmengo committed Jan 27, 2025
    Copy the full SHA
    02b8967 View commit details
  6. Render snippet name on hover when file exists without liquidDoc defin…

    …ition
    jamesmengo committed Jan 27, 2025
    Copy the full SHA
    6f1ddbb View commit details
  7. [LiquidDoc] Prevent rendering of hover tooltip for snippets with no r…

    …enderable content (#729)
    
    ## What are you adding in this PR?
    
    `{% render 'snippet' %}`
    
    Prevents rendering hover tooltip for snippets that:
    - do not exist
    - do not have renderable content.
    
    **Renderable content**: Currently, this means that it requires a `LiquidDoc` definition with `@param` annotations, though this will soon expand to the description and `@example` as well
    
    
    ## Before you deploy
    
    <!-- Delete the checklists you don't need -->
    
    <!-- Check changes -->
    - [ ] This PR includes a new checks or changes the configuration of a check
      - [ ] I included a minor bump `changeset`
      - [ ] It's in the `allChecks` array in `src/checks/index.ts`
      - [ ] I ran `yarn build` and committed the updated configuration files
        <!-- It might be that a check doesn't make sense in a theme-app-extension context -->
        <!-- When that happens, the check's config should be updated/overridden in the theme-app-extension config -->
        <!-- see packages/node/configs/theme-app-extension.yml -->
        - [ ] If applicable, I've updated the `theme-app-extension.yml` config
    
    <!-- Public API changes, new features -->
    - [ ] I included a minor bump `changeset`
    - [x] My feature is backward compatible
    
    <!-- Bug fixes -->
    - [x] I included a patch bump `changeset`
    jamesmengo authored Jan 27, 2025
    Copy the full SHA
    f28e72d View commit details
  8. [LiquidDoc] Modify paramDescription to be null if empty (#737)

    ## What are you adding in this PR?
    
    Returns `null` in the `liquid-html-parser` if `paramDescription` value is `''`. 
    
    Also added some test coverage in the language server for this case.
    
    - [paramDescription](https://github.com/Shopify/theme-tools/blob/main/packages/liquid-html-parser/grammar/liquid-html.ohm#L403) is always matched in the grammar. I experimented with changing that matching logic but ultimately felt it was simpler this logic in instead `stage-2`, though we could revisit this
    
    
    <!-- Bug fixes -->
    - [x] I included a patch bump `changeset`
    jamesmengo authored Jan 27, 2025
    Copy the full SHA
    fced038 View commit details

Commits on Jan 28, 2025

  1. Add duplicate settings ID detection for theme checker

    Implements a new validation check to prevent duplicate setting IDs
    across theme configurations. This helps theme developers identify and
    fix duplicate IDs early in development, preventing potential conflicts
    and overrides.
    
    The implementation includes:
    - New JSONCheckDefinition for unique setting ID validation
    - Test coverage for valid and invalid theme configurations
    - Detection logic for finding duplicate IDs in settings schema
    
    See:
    Ticket: 49020903
    Shopify/cli#4187
    zacariec committed Jan 28, 2025
    Copy the full SHA
    a505e42 View commit details
  2. Copy the full SHA
    0575160 View commit details
  3. Add changeset update config

    zacariec committed Jan 28, 2025
    Copy the full SHA
    913d538 View commit details
  4. [Theme Check] Add check for duplicate settings id's (#696)

    Closes Shopify/developer-tools-team#263
    
    Implements a new validation check to prevent duplicate setting IDs across theme configurations. This helps theme developers identify and fix duplicate IDs early in development, preventing potential conflicts and overrides.
    
    The implementation includes:
    - New JSONCheckDefinition for unique setting ID validation
    - Test coverage for valid and invalid theme configurations
    - Detection logic for finding duplicate IDs in settings schema
    
    See:
    Ticket: 49020903
    Shopify/cli#4187
    
    ## What are you adding in this PR?
    
    This PR adds a new theme check validator that detects duplicate setting IDs in theme configurations. The validator scans settings_schema.json files and reports an error when it finds multiple settings using the same ID value, preventing potential runtime conflicts.
    Key changes:
    
    - New UniqueSettingIds check that validates setting ID uniqueness
    - Integration with the existing theme checker infrastructure
    - Test coverage for both valid and invalid scenarios
    - Error reporting with precise location information
    
    ## What's next? Any followup issues?
    
    Potential follow-ups:
    - Consider extending the check to validate uniqueness across sections
    - Add documentation for theme developers about ID uniqueness requirements
    - Consider adding quick-fix suggestions for duplicate IDs
    
    ## What did you learn?
    
    Working with AST traversal for JSON validation showed that we need a consistent approach to handle nested property validation. The existing node type checks provided a good pattern to follow.
    
    ## Before you deploy
    
    <!-- Delete the checklists you don't need -->
    
    <!-- Check changes -->
    - [x] This PR includes a new checks or changes the configuration of a check
    - [x] I included a minor bump `changeset`
    - [x] It's in the `allChecks` array in `src/checks/index.ts`
    - [x] I ran `yarn build` and committed the updated configuration files
    zacariec authored Jan 28, 2025
    Copy the full SHA
    ed3c437 View commit details
  5. Add npm provenance to changeset workflow (#739)

    charlespwd authored Jan 28, 2025
    Copy the full SHA
    915ccf5 View commit details
  6. Add contents permissions to release workflow (#740)

    charlespwd authored Jan 28, 2025
    Copy the full SHA
    c3300f9 View commit details
  7. Add pull-requests: write too (#741)

    charlespwd authored Jan 28, 2025
    Copy the full SHA
    a2e7f70 View commit details
  8. Copy the full SHA
    a8a7cc1 View commit details

Commits on Jan 30, 2025

  1. Merge pull request #731 from Shopify/changeset-release/main

    Theme Tools Release — 2025-01-28
    graygilmore authored Jan 30, 2025
    Copy the full SHA
    eec8b27 View commit details
  2. Update repository URL definition to be case sensitive

    Without this we'll fail to publish to NPM due to a provenance failure
    that looks like this:
    
    ```
    error an error occurred while publishing
    @shopify/theme-language-server-browser: E422 422 Unprocessable Entity -
    PUT https://registry.npmjs.org/@shopify%2ftheme-language-server-browser
    - Error verifying sigstore provenance bundle: Failed to validate
      repository information: package.json: "repository.url" is
    "git+https://github.com/shopify/theme-tools.git", expected to match
    "https://github.com/Shopify/theme-tools" from provenance
    ```
    graygilmore committed Jan 30, 2025
    Copy the full SHA
    841ca6d View commit details

Commits on Jan 31, 2025

  1. Merge pull request #746 from Shopify/gg-fix-repository-url

    Update repository URL definition to be case sensitive
    graygilmore authored Jan 31, 2025
    Copy the full SHA
    d6286fb View commit details
  2. Copy the full SHA
    9173015 View commit details
  3. Merge pull request #747 from Shopify/changeset-release/main

    Theme Tools Release — 2025-01-31
    graygilmore authored Jan 31, 2025
    Copy the full SHA
    8bceaa3 View commit details
Showing with 2,367 additions and 396 deletions.
  1. +5 −0 .github/workflows/release.yml
  2. +6 −0 packages/codemirror-language-client/CHANGELOG.md
  3. +6 −6 packages/codemirror-language-client/package.json
  4. +6 −0 packages/lang-jsonc/CHANGELOG.md
  5. +3 −3 packages/lang-jsonc/package.json
  6. +12 −0 packages/liquid-html-parser/CHANGELOG.md
  7. +3 −3 packages/liquid-html-parser/package.json
  8. +18 −6 packages/liquid-html-parser/src/stage-2-ast.spec.ts
  9. +2 −1 packages/liquid-html-parser/src/stage-2-ast.ts
  10. +16 −0 packages/prettier-plugin-liquid/CHANGELOG.md
  11. +4 −4 packages/prettier-plugin-liquid/package.json
  12. +17 −0 packages/theme-check-browser/CHANGELOG.md
  13. +4 −4 packages/theme-check-browser/package.json
  14. +21 −0 packages/theme-check-common/CHANGELOG.md
  15. +4 −4 packages/theme-check-common/package.json
  16. +2 −0 packages/theme-check-common/src/checks/index.ts
  17. +24 −0 packages/theme-check-common/src/checks/unique-settings-id/index.spec.ts
  18. +84 −0 packages/theme-check-common/src/checks/unique-settings-id/index.ts
  19. +1,191 −0 packages/theme-check-common/src/checks/unique-settings-id/test-data.ts
  20. +4 −0 packages/theme-check-common/src/types.ts
  21. +17 −0 packages/theme-check-docs-updater/CHANGELOG.md
  22. +4 −4 packages/theme-check-docs-updater/package.json
  23. +19 −0 packages/theme-check-node/CHANGELOG.md
  24. +3 −0 packages/theme-check-node/configs/all.yml
  25. +3 −0 packages/theme-check-node/configs/recommended.yml
  26. +5 −5 packages/theme-check-node/package.json
  27. +19 −0 packages/theme-language-server-browser/CHANGELOG.md
  28. +4 −4 packages/theme-language-server-browser/package.json
  29. +31 −0 packages/theme-language-server-common/CHANGELOG.md
  30. +5 −5 packages/theme-language-server-common/package.json
  31. +8 −0 packages/theme-language-server-common/src/documents/DocumentManager.ts
  32. +2 −0 packages/theme-language-server-common/src/documents/types.ts
  33. +9 −0 packages/theme-language-server-common/src/hover/HoverProvider.ts
  34. +90 −0 packages/theme-language-server-common/src/hover/providers/RenderSnippetHoverProvider.spec.ts
  35. +70 −0 packages/theme-language-server-common/src/hover/providers/RenderSnippetHoverProvider.ts
  36. +1 −0 packages/theme-language-server-common/src/hover/providers/index.ts
  37. +4 −4 packages/theme-language-server-common/src/json/JSONContributions.ts
  38. +2 −4 packages/theme-language-server-common/src/json/completions/providers/BlockTypeCompletionProvider.ts
  39. +4 −6 ...theme-language-server-common/src/json/completions/providers/PresetsBlockTypeCompletionProvider.ts
  40. +0 −161 ...ge-server-common/src/json/completions/providers/PresetsSettingsPropertyCompletionProvider.spec.ts
  41. +2 −4 ...heme-language-server-common/src/json/completions/providers/SchemaTranslationCompletionProvider.ts
  42. +211 −0 ...-language-server-common/src/json/completions/providers/SettingsPropertyCompletionProvider.spec.ts
  43. +25 −11 .../providers/{PresetsSettingsPropertyCompletionProvider.ts → SettingsPropertyCompletionProvider.ts}
  44. +0 −119 packages/theme-language-server-common/src/json/hover/providers/PresetsSettingsHoverProvider.spec.ts
  45. +2 −4 packages/theme-language-server-common/src/json/hover/providers/SchemaTranslationHoverProvider.ts
  46. +139 −0 packages/theme-language-server-common/src/json/hover/providers/SettingsHoverProvider.spec.ts
  47. +14 −8 ...rver-common/src/json/hover/providers/{PresetsSettingsHoverProvider.ts → SettingsHoverProvider.ts}
  48. +12 −0 packages/theme-language-server-common/src/json/utils.ts
  49. +74 −0 packages/theme-language-server-common/src/liquidDoc.spec.ts
  50. +50 −0 packages/theme-language-server-common/src/liquidDoc.ts
  51. +17 −0 packages/theme-language-server-common/src/server/startServer.ts
  52. +23 −0 packages/theme-language-server-node/CHANGELOG.md
  53. +6 −6 packages/theme-language-server-node/package.json
  54. +19 −11 packages/theme-language-server-node/src/metafieldDefinitions.ts
  55. +32 −0 packages/vscode-extension/CHANGELOG.md
  56. +9 −9 packages/vscode-extension/package.json
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -16,6 +16,10 @@ env:
jobs:
publish-packages:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write

steps:
- uses: actions/checkout@v4
@@ -64,6 +68,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

- name: VS Code Marketplace publish
if: steps.changesets.outputs.hasChangesets == 'false' && steps.marketplace-version.outputs.version != steps.package-version.outputs.version
6 changes: 6 additions & 0 deletions packages/codemirror-language-client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @shopify/codemirror-language-client

## 0.7.3

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive

## 0.7.2

### Patch Changes
12 changes: 6 additions & 6 deletions packages/codemirror-language-client/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "@shopify/codemirror-language-client",
"version": "0.7.2",
"version": "0.7.3",
"description": "A Language Client for CodeMirror",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/codemirror-language-client#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/codemirror-language-client"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"main": "./dist/umd/index.js",
@@ -51,9 +51,9 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.9.2",
"@replit/codemirror-vim": "^6.2.1",
"@shopify/theme-check-docs-updater": "^3.6.1",
"@shopify/theme-language-server-browser": "^2.5.0",
"@shopify/lang-jsonc": "^1.0.0",
"@shopify/theme-check-docs-updater": "^3.7.1",
"@shopify/theme-language-server-browser": "^2.6.1",
"@shopify/lang-jsonc": "^1.0.1",
"@types/markdown-it": "^13.0.4",
"codemirror": "^6.0.1",
"markdown-it": "^13.0.2"
6 changes: 6 additions & 0 deletions packages/lang-jsonc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @shopify/lang-jsonc

## 1.0.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive

## 1.0.0

### Major Changes
6 changes: 3 additions & 3 deletions packages/lang-jsonc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shopify/lang-jsonc",
"version": "1.0.0",
"version": "1.0.1",
"description": "JSONC language support for CodeMirror",
"publishConfig": {
"access": "public",
@@ -10,11 +10,11 @@
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/lang-jsonc#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/lang-jsonc"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"main": "./dist/umd/index.js",
"typings": "./dist/umd/index.js",
12 changes: 12 additions & 0 deletions packages/liquid-html-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @shopify/liquid-html-parser

## 2.3.2

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive

## 2.3.1

### Patch Changes

- dd0cd4d2: [Internal] Modify paramDescription to be null when empty

## 2.3.0

### Minor Changes
6 changes: 3 additions & 3 deletions packages/liquid-html-parser/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "@shopify/liquid-html-parser",
"version": "2.3.0",
"version": "2.3.2",
"description": "Liquid HTML parser by Shopify",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/liquid-html-parser#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/liquid-html-parser"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"main": "dist/index.js",
24 changes: 18 additions & 6 deletions packages/liquid-html-parser/src/stage-2-ast.spec.ts
Original file line number Diff line number Diff line change
@@ -1231,19 +1231,22 @@ describe('Unit: Stage 2 (AST)', () => {

ast = toLiquidAST(`
{% doc -%}
@param asdf
@param paramWithNoType
@param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description.
@param {String} paramWithNoDescription
@unsupported this node falls back to a text node
{%- enddoc %}
`);
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
expectPath(ast, 'children.0.name').to.eql('doc');

expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('asdf');
expectPath(ast, 'children.0.body.nodes.0.paramDescription.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql('');
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('paramWithNoType');
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;

expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
@@ -1254,8 +1257,17 @@ describe('Unit: Stage 2 (AST)', () => {
);
expectPath(ast, 'children.0.body.nodes.1.paramType.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.1.paramType.value').to.eql('String');
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.value').to.eql(

expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithNoDescription');
expectPath(ast, 'children.0.body.nodes.2.paramDescription').to.be.null;
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');

expectPath(ast, 'children.0.body.nodes.3.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.3.value').to.eql(
'@unsupported this node falls back to a text node',
);
});
3 changes: 2 additions & 1 deletion packages/liquid-html-parser/src/stage-2-ast.ts
Original file line number Diff line number Diff line change
@@ -1974,7 +1974,8 @@ function toHtmlSelfClosingElement(
}

function toNullableTextNode(node: ConcreteTextNode | null): TextNode | null {
if (!node) return null;
if (!node || node.value === '') return null;

return toTextNode(node);
}

16 changes: 16 additions & 0 deletions packages/prettier-plugin-liquid/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# @shopify/prettier-plugin-liquid

## 1.7.2

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/liquid-html-parser@2.3.2

## 1.7.1

### Patch Changes

- Patch bump because it depends on @shopify/liquid-html-parser
- Updated dependencies [dd0cd4d2]
- @shopify/liquid-html-parser@2.3.1

## 1.7.0

### Minor Changes
8 changes: 4 additions & 4 deletions packages/prettier-plugin-liquid/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "@shopify/prettier-plugin-liquid",
"version": "1.7.0",
"version": "1.7.2",
"description": "Prettier Liquid/HTML plugin by Shopify",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/prettier-plugin-liquid#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/prettier-plugin-liquid"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"main": "dist/index.js",
@@ -59,7 +59,7 @@
"tsconfig-paths": "^3.14.1"
},
"dependencies": {
"@shopify/liquid-html-parser": "^2.3.0",
"@shopify/liquid-html-parser": "^2.3.2",
"html-styles": "^1.0.0"
}
}
17 changes: 17 additions & 0 deletions packages/theme-check-browser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# @shopify/theme-check-browser

## 3.7.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-check-common@3.7.1

## 3.7.0

### Patch Changes

- Updated dependencies [c85a6131]
- Updated dependencies [913d5386]
- Updated dependencies [931dc9b9]
- @shopify/theme-check-common@3.7.0

## 3.6.1

### Patch Changes
8 changes: 4 additions & 4 deletions packages/theme-check-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "@shopify/theme-check-browser",
"version": "3.6.1",
"version": "3.7.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-check-browser#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-check-browser"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"publishConfig": {
"access": "public",
@@ -26,6 +26,6 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/theme-check-common": "3.6.1"
"@shopify/theme-check-common": "3.7.1"
}
}
21 changes: 21 additions & 0 deletions packages/theme-check-common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# @shopify/theme-check-common

## 3.7.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/liquid-html-parser@2.3.2

## 3.7.0

### Minor Changes

- c85a6131: Cache liquidDoc fetch results
- 913d5386: Add unique settings ID validator to prevent duplicate IDs in theme configurations
- 931dc9b9: Add hover support for Liquid snippets using {% doc %} annotations

### Patch Changes

- Updated dependencies [dd0cd4d2]
- @shopify/liquid-html-parser@2.3.1

## 3.6.1

### Patch Changes
8 changes: 4 additions & 4 deletions packages/theme-check-common/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "@shopify/theme-check-common",
"version": "3.6.1",
"version": "3.7.1",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-check-common#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-check-common"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"publishConfig": {
"access": "public",
@@ -26,7 +26,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/liquid-html-parser": "2.3.0",
"@shopify/liquid-html-parser": "2.3.2",
"cross-fetch": "^4.0.0",
"jsonc-parser": "^3.2.0",
"line-column": "^1.0.2",
2 changes: 2 additions & 0 deletions packages/theme-check-common/src/checks/index.ts
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ import { ValidSchemaName } from './valid-schema-name';
import { ValidStaticBlockType } from './valid-static-block-type';
import { VariableName } from './variable-name';
import { AppBlockMissingSchema } from './app-block-missing-schema';
import { UniqueSettingIds } from './unique-settings-id';

export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
AppBlockValidTags,
@@ -80,6 +81,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
TranslationKeyExists,
UnclosedHTMLElement,
UndefinedObject,
UniqueSettingIds,
UniqueStaticBlockId,
UnknownFilter,
UnusedAssign,
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, describe, it } from 'vitest';
import { UniqueSettingIds } from './index';
import { highlightedOffenses, runJSONCheck } from '../../test';
import { invalidJson, validJson } from './test-data';

describe('Module: UniqueSettingIds', () => {
it("Should report an error for duplicate id's in settings_schema (0)", async () => {
const offenses = await runJSONCheck(UniqueSettingIds, invalidJson, 'file.json');

expect(offenses).to.have.length(1);
expect(offenses[0].message).to.equal('Duplicate setting id found: "nosto_account_id"');

const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses);

expect(highlights).to.have.length(1);
expect(highlights[0]).to.equal('"id": "nosto_account_id"');
});

it('should not report any errors for valid file', async () => {
const offenses = await runJSONCheck(UniqueSettingIds, validJson);

expect(offenses).to.be.empty;
});
});
84 changes: 84 additions & 0 deletions packages/theme-check-common/src/checks/unique-settings-id/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
isArrayNode,
isLiteralNode,
isObjectNode,
isPropertyNode,
Severity,
SourceCodeType,
} from '../../types';

import type { ArrayNode, PropertyNode, JSONCheckDefinition } from '../../types';

export const UniqueSettingIds: JSONCheckDefinition = {
meta: {
code: 'UniqueSettingId',
name: 'Prevent duplicate Ids in setting_schema',
docs: {
description: 'This check is aimed at eliminating duplicate Ids in settings_schema.json',
recommended: true,
// url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/valid-schema',
},
type: SourceCodeType.JSON,
severity: Severity.ERROR,
schema: {},
targets: [],
},

create(context) {
return {
async onCodePathEnd(file) {
if (isArrayNode(file.ast)) {
const settingIds: PropertyNode[] = [];

/* Find and loop through all of our nodes that have an id value and find their key value */
for (const child of file.ast.children) {
if (isObjectNode(child) && child.children) {
const settingsNode = child.children.find((node) => node.key.value === 'settings');

if (settingsNode && settingsNode.value && isArrayNode(settingsNode.value)) {
for (const setting of settingsNode.value.children) {
if (isObjectNode(setting) && setting.children) {
const idNode = setting.children.find((node) => node.key.value === 'id');
if (isPropertyNode(idNode)) {
settingIds.push(idNode);
}
}
}
}
}
}

/* Check for dupes */
const idMap = new Map<string, PropertyNode[]>();
for (const node of settingIds) {
if (isLiteralNode(node.value)) {
const id = node.value.value;
if (typeof id === 'string') {
if (!idMap.has(id)) {
idMap.set(id, []);
}
idMap.get(id)!.push(node);
}
}
}

const duplicates: [string, PropertyNode[]][] = Array.from(idMap.entries()).filter(
([_, nodes]) => nodes.length > 1,
);

if (duplicates.length > 0) {
for (const [id, nodes] of duplicates) {
const lastNodeFound = nodes[nodes.length - 1];

context.report({
message: `Duplicate setting id found: "${id}"`,
startIndex: lastNodeFound.loc.start.offset,
endIndex: lastNodeFound.loc.end.offset,
});
}
}
}
},
};
},
};
1,191 changes: 1,191 additions & 0 deletions packages/theme-check-common/src/checks/unique-settings-id/test-data.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/theme-check-common/src/types.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,10 @@ import {
ASTNode,
JSONNode,
JSONNodeTypes,
LiteralNode,
ObjectNode,
PropertyNode,
ValueNode,
} from './jsonc/types';
import { JsonValidationSet, ThemeDocset } from './types/theme-liquid-docs';
import { AppBlockSchema, SectionSchema, ThemeBlockSchema } from './types/theme-schemas';
@@ -24,6 +26,8 @@ export * from './types/theme-schemas';
export const isObjectNode = (node?: ASTNode): node is ObjectNode => node?.type === 'Object';
export const isArrayNode = (node?: ASTNode): node is ArrayNode => node?.type === 'Array';
export const isPropertyNode = (node?: ASTNode): node is PropertyNode => node?.type === 'Property';
export const isValueNode = (node?: ASTNode): node is ValueNode => node?.type === 'Value';
export const isLiteralNode = (node?: ASTNode): node is LiteralNode => node?.type === 'Literal';

export const Modes = ['theme', 'app'] as const;
export type Mode = (typeof Modes)[number];
17 changes: 17 additions & 0 deletions packages/theme-check-docs-updater/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# @shopify/theme-check-docs-updater

## 3.7.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-check-common@3.7.1

## 3.7.0

### Patch Changes

- Updated dependencies [c85a6131]
- Updated dependencies [913d5386]
- Updated dependencies [931dc9b9]
- @shopify/theme-check-common@3.7.0

## 3.6.1

### Patch Changes
8 changes: 4 additions & 4 deletions packages/theme-check-docs-updater/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "@shopify/theme-check-docs-updater",
"version": "3.6.1",
"version": "3.7.1",
"description": "Scripts to initialize theme-check data with assets from the theme-liquid-docs repo.",
"main": "dist/index.js",
"author": "Albert Chu <albert.chu@shopify.com>",
"license": "MIT",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-check-docs-updater#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-check-docs-updater"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/theme-check-common": "^3.6.1",
"@shopify/theme-check-common": "^3.7.1",
"env-paths": "^2.2.1",
"node-fetch": "^2.6.11"
},
19 changes: 19 additions & 0 deletions packages/theme-check-node/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# @shopify/theme-check-node

## 3.7.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-check-docs-updater@3.7.1
- @shopify/theme-check-common@3.7.1

## 3.7.0

### Patch Changes

- Updated dependencies [c85a6131]
- Updated dependencies [913d5386]
- Updated dependencies [931dc9b9]
- @shopify/theme-check-common@3.7.0
- @shopify/theme-check-docs-updater@3.7.0

## 3.6.1

### Patch Changes
3 changes: 3 additions & 0 deletions packages/theme-check-node/configs/all.yml
Original file line number Diff line number Diff line change
@@ -109,6 +109,9 @@ UnclosedHTMLElement:
UndefinedObject:
enabled: true
severity: 1
UniqueSettingId:
enabled: true
severity: 0
UniqueStaticBlockId:
enabled: true
severity: 0
3 changes: 3 additions & 0 deletions packages/theme-check-node/configs/recommended.yml
Original file line number Diff line number Diff line change
@@ -87,6 +87,9 @@ UnclosedHTMLElement:
UndefinedObject:
enabled: true
severity: 1
UniqueSettingId:
enabled: true
severity: 0
UniqueStaticBlockId:
enabled: true
severity: 0
10 changes: 5 additions & 5 deletions packages/theme-check-node/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@shopify/theme-check-node",
"version": "3.6.1",
"version": "3.7.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-check-node#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-check-node"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"publishConfig": {
@@ -33,8 +33,8 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/theme-check-common": "3.6.1",
"@shopify/theme-check-docs-updater": "3.6.1",
"@shopify/theme-check-common": "3.7.1",
"@shopify/theme-check-docs-updater": "3.7.1",
"glob": "^8.0.3",
"vscode-uri": "^3.0.7",
"yaml": "^2.3.0"
19 changes: 19 additions & 0 deletions packages/theme-language-server-browser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# @shopify/theme-language-server-browser

## 2.6.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-language-server-common@2.6.1

## 2.6.0

### Patch Changes

- Updated dependencies [c85a6131]
- Updated dependencies [931dc9b9]
- Updated dependencies [02b8967f]
- Updated dependencies [9765bece]
- Updated dependencies [dd0cd4d2]
- @shopify/theme-language-server-common@2.6.0

## 2.5.0

### Patch Changes
8 changes: 4 additions & 4 deletions packages/theme-language-server-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@shopify/theme-language-server-browser",
"version": "2.5.0",
"version": "2.6.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-language-server-browser#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-language-server-browser"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"publishConfig": {
@@ -27,7 +27,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/theme-language-server-common": "2.5.0",
"@shopify/theme-language-server-common": "2.6.1",
"vscode-languageserver": "^8.0.2"
}
}
31 changes: 31 additions & 0 deletions packages/theme-language-server-common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# @shopify/theme-language-server-common

## 2.6.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/liquid-html-parser@2.3.2
- @shopify/theme-check-common@3.7.1

## 2.6.0

### Minor Changes

- c85a6131: Cache liquidDoc fetch results
- 931dc9b9: Add hover support for Liquid snippets using {% doc %} annotations
- 9765bece: Support `default.settings` completion + hover

- Fix bug where `presets.[].settings` hover only worked on first setting
- Fix bug where preset block completion creates an error when `blocks` isn't defined

### Patch Changes

- 02b8967f: Prevent rendering hover tooltip for snippets that do not renderable content
- dd0cd4d2: [Internal] Modify paramDescription to be null when empty
- Updated dependencies [c85a6131]
- Updated dependencies [913d5386]
- Updated dependencies [931dc9b9]
- Updated dependencies [dd0cd4d2]
- @shopify/theme-check-common@3.7.0
- @shopify/liquid-html-parser@2.3.1

## 2.5.0

### Minor Changes
10 changes: 5 additions & 5 deletions packages/theme-language-server-common/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@shopify/theme-language-server-common",
"version": "2.5.0",
"version": "2.6.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-language-server-common#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-language-server-common"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"publishConfig": {
@@ -27,8 +27,8 @@
"type-check": "tsc --noEmit -p src/tsconfig.json"
},
"dependencies": {
"@shopify/liquid-html-parser": "^2.3.0",
"@shopify/theme-check-common": "3.6.1",
"@shopify/liquid-html-parser": "^2.3.2",
"@shopify/theme-check-common": "3.7.1",
"@vscode/web-custom-data": "^0.4.6",
"vscode-json-languageservice": "^5.3.10",
"vscode-languageserver": "^8.0.2",
Original file line number Diff line number Diff line change
@@ -12,12 +12,14 @@ import {
IsValidSchema,
memo,
Mode,
isError,
} from '@shopify/theme-check-common';
import { Connection } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { ClientCapabilities } from '../ClientCapabilities';
import { percent, Progress } from '../progress';
import { AugmentedSourceCode } from './types';
import { getSnippetDefinition } from '../liquidDoc';

export class DocumentManager {
/**
@@ -171,6 +173,12 @@ export class DocumentManager {
const mode = await this.getModeForUri!(uri);
return toSchema(mode, uri, sourceCode, this.isValidSchema);
}),
/** Lazy and only computed once per file version */
getLiquidDoc: memo(async (snippetName: string) => {
if (isError(sourceCode.ast)) return undefined;

return getSnippetDefinition(sourceCode.ast, snippetName);
}),
};
default:
return assertNever(sourceCode);
2 changes: 2 additions & 0 deletions packages/theme-language-server-common/src/documents/types.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import {
AppBlockSchema,
} from '@shopify/theme-check-common';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { SnippetDefinition } from '../liquidDoc';

/** Util type to add the common `textDocument` property to the SourceCode. */
type _AugmentedSourceCode<SCT extends SourceCodeType = SourceCodeType> = SourceCode<SCT> & {
@@ -23,6 +24,7 @@ export type AugmentedJsonSourceCode = _AugmentedSourceCode<SourceCodeType.JSON>;
*/
export type AugmentedLiquidSourceCode = _AugmentedSourceCode<SourceCodeType.LiquidHtml> & {
getSchema: () => Promise<SectionSchema | ThemeBlockSchema | AppBlockSchema | undefined>;
getLiquidDoc: (snippetName: string) => Promise<SnippetDefinition | undefined>;
};

/**
Original file line number Diff line number Diff line change
@@ -12,10 +12,12 @@ import {
LiquidObjectHoverProvider,
LiquidTagHoverProvider,
TranslationHoverProvider,
RenderSnippetHoverProvider,
} from './providers';
import { HtmlAttributeValueHoverProvider } from './providers/HtmlAttributeValueHoverProvider';
import { findCurrentNode } from '@shopify/theme-check-common';
import { GetThemeSettingsSchemaForURI } from '../settings';
import { GetSnippetDefinitionForURI } from '../liquidDoc';

export class HoverProvider {
private providers: BaseHoverProvider[] = [];
@@ -26,6 +28,12 @@ export class HoverProvider {
readonly getMetafieldDefinitions: (rootUri: string) => Promise<MetafieldDefinitionMap>,
readonly getTranslationsForURI: GetTranslationsForURI = async () => ({}),
readonly getSettingsSchemaForURI: GetThemeSettingsSchemaForURI = async () => [],
readonly getSnippetDefinitionForURI: GetSnippetDefinitionForURI = async (
_uri,
snippetName,
) => ({
name: snippetName,
}),
) {
const typeSystem = new TypeSystem(
themeDocset,
@@ -41,6 +49,7 @@ export class HoverProvider {
new HtmlAttributeHoverProvider(),
new HtmlAttributeValueHoverProvider(),
new TranslationHoverProvider(getTranslationsForURI, documentManager),
new RenderSnippetHoverProvider(getSnippetDefinitionForURI),
];
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, beforeEach, it, expect } from 'vitest';
import { DocumentManager } from '../../documents';
import { HoverProvider } from '../HoverProvider';
import { MetafieldDefinitionMap } from '@shopify/theme-check-common';
import { GetSnippetDefinitionForURI, SnippetDefinition } from '../../liquidDoc';
import '../../../../theme-check-common/src/test/test-setup';

describe('Module: RenderSnippetHoverProvider', async () => {
let provider: HoverProvider;
let getSnippetDefinition: GetSnippetDefinitionForURI;
const mockSnippetDefinition: SnippetDefinition = {
name: 'product-card',
liquidDoc: {
parameters: [
{
name: 'title',
description: 'The title of the product',
type: 'string',
},
{
name: 'border-radius',
description: 'The border radius in px',
type: 'number',
},
{
name: 'no-type',
description: 'This parameter has no type',
type: null,
},
{
name: 'no-description',
description: null,
type: 'string',
},
{
name: 'no-type-or-description',
description: null,
type: null,
},
],
},
};

const createProvider = (getSnippetDefinition: GetSnippetDefinitionForURI) => {
return new HoverProvider(
new DocumentManager(),
{
filters: async () => [],
objects: async () => [],
tags: async () => [],
systemTranslations: async () => ({}),
},
async (_rootUri: string) => ({} as MetafieldDefinitionMap),
async () => ({}),
async () => [],
getSnippetDefinition,
);
};

beforeEach(async () => {
getSnippetDefinition = async () => mockSnippetDefinition;
provider = createProvider(getSnippetDefinition);
});

describe('hover', () => {
it('should return snippet definition with all parameters', async () => {
await expect(provider).to.hover(
`{% render 'product-car█d' %}`,
'### product-card\n\n**Parameters:**\n- `title`: string - The title of the product\n- `border-radius`: number - The border radius in px\n- `no-type` - This parameter has no type\n- `no-description`: string\n- `no-type-or-description`',
);
});

it('should return null if no LiquidDocDefinition found', async () => {
getSnippetDefinition = async () => ({ name: 'unknown-snippet', liquidDoc: undefined });
provider = createProvider(getSnippetDefinition);
await expect(provider).to.hover(`{% render 'unknown-sni█ppet' %}`, `### unknown-snippet`);
});

it('should return null if snippet is null', async () => {
getSnippetDefinition = async () => undefined;
provider = createProvider(getSnippetDefinition);
await expect(provider).to.hover(`{% render 'unknown-sni█ppet' %}`, null);
});

it('should return nothing if not in render tag', async () => {
await expect(provider).to.hover(`{% assign asdf = 'snip█pet' %}`, null);
await expect(provider).to.hover(`{{ 'snip█pet' }}`, null);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NodeTypes } from '@shopify/liquid-html-parser';
import { LiquidHtmlNode } from '@shopify/theme-check-common';
import { Hover, HoverParams } from 'vscode-languageserver';
import { BaseHoverProvider } from '../BaseHoverProvider';
import { SnippetDefinition, LiquidDocParameter } from '../../liquidDoc';

export class RenderSnippetHoverProvider implements BaseHoverProvider {
constructor(
private getSnippetDefinitionForURI: (
uri: string,
snippetName: string,
) => Promise<SnippetDefinition | undefined>,
) {}

async hover(
currentNode: LiquidHtmlNode,
ancestors: LiquidHtmlNode[],
params: HoverParams,
): Promise<Hover | null> {
const parentNode = ancestors.at(-1);
if (
currentNode.type !== NodeTypes.String ||
!parentNode ||
parentNode.type !== NodeTypes.RenderMarkup
) {
return null;
}

const snippetName = currentNode.value;
const snippetDefinition = await this.getSnippetDefinitionForURI(
params.textDocument.uri,
snippetName,
);

if (!snippetDefinition) {
return null;
}

const liquidDoc = snippetDefinition.liquidDoc;

if (!liquidDoc) {
return {
contents: {
kind: 'markdown',
value: `### ${snippetDefinition.name}`,
},
};
}

const parts = [`### ${snippetDefinition.name}`];

if (liquidDoc.parameters?.length) {
const parameters = liquidDoc.parameters
?.map(
({ name, type, description }: LiquidDocParameter) =>
`- \`${name}\`${type ? `: ${type}` : ''}${description ? ` - ${description}` : ''}`,
)
.join('\n');

parts.push('', '**Parameters:**', parameters);
}

return {
contents: {
kind: 'markdown',
value: parts.join('\n'),
},
};
}
}
Original file line number Diff line number Diff line change
@@ -6,3 +6,4 @@ export { HtmlTagHoverProvider } from './HtmlTagHoverProvider';
export { HtmlAttributeHoverProvider } from './HtmlAttributeHoverProvider';
export { HtmlAttributeValueHoverProvider } from './HtmlAttributeValueHoverProvider';
export { TranslationHoverProvider } from './TranslationHoverProvider';
export { RenderSnippetHoverProvider } from './RenderSnippetHoverProvider';
Original file line number Diff line number Diff line change
@@ -22,8 +22,8 @@ import { SchemaTranslationHoverProvider } from './hover/providers/SchemaTranslat
import { TranslationPathHoverProvider } from './hover/providers/TranslationPathHoverProvider';
import { RequestContext } from './RequestContext';
import { findSchemaNode } from './utils';
import { PresetsSettingsPropertyCompletionProvider } from './completions/providers/PresetsSettingsPropertyCompletionProvider';
import { PresetsSettingsHoverProvider } from './hover/providers/PresetsSettingsHoverProvider';
import { SettingsPropertyCompletionProvider } from './completions/providers/SettingsPropertyCompletionProvider';
import { SettingsHoverProvider } from './hover/providers/SettingsHoverProvider';

/** The getInfoContribution API will only fallback if we return undefined synchronously */
const SKIP_CONTRIBUTION = undefined as any;
@@ -54,13 +54,13 @@ export class JSONContributions implements JSONWorkerContribution {
this.hoverProviders = [
new TranslationPathHoverProvider(),
new SchemaTranslationHoverProvider(getDefaultSchemaTranslations),
new PresetsSettingsHoverProvider(getDefaultSchemaTranslations),
new SettingsHoverProvider(getDefaultSchemaTranslations),
];
this.completionProviders = [
new SchemaTranslationsCompletionProvider(getDefaultSchemaTranslations),
new BlockTypeCompletionProvider(getThemeBlockNames),
new PresetsBlockTypeCompletionProvider(getThemeBlockNames, getThemeBlockSchema),
new PresetsSettingsPropertyCompletionProvider(getDefaultSchemaTranslations),
new SettingsPropertyCompletionProvider(getDefaultSchemaTranslations),
];
}

Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { JSONPath } from 'vscode-json-languageservice';
import { JSONCompletionItem } from 'vscode-json-languageservice/lib/umd/jsonContributions';
import { CompletionItemKind } from 'vscode-languageserver-protocol';
import { isLiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isSectionOrBlockFile } from '../../utils';
import { JSONCompletionProvider } from '../JSONCompletionProvider';
import { GetThemeBlockNames } from '../../JSONContributions';

@@ -28,13 +28,11 @@ import { GetThemeBlockNames } from '../../JSONContributions';
* {% endschema %}
*/
export class BlockTypeCompletionProvider implements JSONCompletionProvider {
private uriPatterns = [/^.*\/(sections|blocks)\/[^\/]*\.liquid$/];

constructor(private getThemeBlockNames: GetThemeBlockNames) {}

async completeValue(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
if (
!fileMatch(context.doc.uri, this.uriPatterns) ||
!isSectionOrBlockFile(context.doc.uri) ||
!isLiquidRequestContext(context) ||
!isBlockDefinitionPath(path)
) {
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { deepGet, isError } from '@shopify/theme-check-common';
import { JSONPath } from 'vscode-json-languageservice';
import { JSONCompletionItem } from 'vscode-json-languageservice/lib/umd/jsonContributions';
import { isLiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isSectionOrBlockFile } from '../../utils';
import { JSONCompletionProvider } from '../JSONCompletionProvider';
import {
createBlockNameCompletionItems,
@@ -12,7 +12,7 @@ import { GetThemeBlockNames, GetThemeBlockSchema } from '../../JSONContributions

/**
* The PresetsBlockTypeCompletionProvider offers value completions of the
* `presets.[](recursive .blocks.[].type)` keys inside section and theme block `{% schema %}` tags.
* `presets.[](recursive .blocks.[]).type` keys inside section and theme block `{% schema %}` tags.
*
* @example
* {% schema %}
@@ -28,16 +28,14 @@ import { GetThemeBlockNames, GetThemeBlockSchema } from '../../JSONContributions
* {% endschema %}
*/
export class PresetsBlockTypeCompletionProvider implements JSONCompletionProvider {
private uriPatterns = [/^.*\/(sections|blocks)\/[^\/]*\.liquid$/];

constructor(
private getThemeBlockNames: GetThemeBlockNames,
private getThemeBlockSchema: GetThemeBlockSchema,
) {}

async completeValue(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
if (
!fileMatch(context.doc.uri, this.uriPatterns) ||
!isSectionOrBlockFile(context.doc.uri) ||
!isLiquidRequestContext(context) ||
!isPresetBlockPath(path)
) {
@@ -72,7 +70,7 @@ export class PresetsBlockTypeCompletionProvider implements JSONCompletionProvide
parsedBlockSchema = parentBlockSchema.parsed;
}

const blocks: { type: string }[] = parsedBlockSchema.blocks;
const blocks: { type: string }[] = parsedBlockSchema.blocks || [];

const blockGroups = {
themeBlocks: false,

This file was deleted.

Original file line number Diff line number Diff line change
@@ -6,16 +6,14 @@ import {
translationOptions,
} from '../../../translations';
import { isLiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isSectionOrBlockFile } from '../../utils';
import { JSONCompletionItem, JSONCompletionProvider } from '../JSONCompletionProvider';

export class SchemaTranslationsCompletionProvider implements JSONCompletionProvider {
private uriPatterns = [/(sections|blocks)\/[^\/]*\.liquid$/];

constructor(private getDefaultSchemaTranslations: GetTranslationsForURI) {}

async completeValue(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
if (!fileMatch(context.doc.uri, this.uriPatterns) || !isLiquidRequestContext(context)) {
if (!isSectionOrBlockFile(context.doc.uri) || !isLiquidRequestContext(context)) {
return [];
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { describe, it, expect, assert, beforeEach } from 'vitest';
import { JSONLanguageService } from '../../JSONLanguageService';
import { DocumentManager } from '../../../documents';
import {
getRequestParams,
isCompletionList,
mockJSONLanguageService,
} from '../../test/test-helpers';

describe('Unit: SettingsPropertyCompletionProvider', () => {
const rootUri = 'file:///root/';
let jsonLanguageService: JSONLanguageService;
let documentManager: DocumentManager;

beforeEach(async () => {
documentManager = new DocumentManager(
undefined, // don't need a fs
undefined, // don't need a connection
undefined, // don't need client capabilities
async () => 'theme', // 'theme' mode
async () => false, // schema is invalid for tests
);
jsonLanguageService = mockJSONLanguageService(
rootUri,
documentManager,
async (_uri: string) => ({
general: {
fake: 'Fake Setting',
},
}),
);

await jsonLanguageService.setup({
textDocument: {
completion: {
contextSupport: true,
completionItem: {
snippetSupport: true,
commitCharactersSupport: true,
documentationFormat: ['markdown'],
deprecatedSupport: true,
preselectSupport: true,
},
},
},
});
});

describe('section file with valid schema', () => {
const tests = [
{
label: 'preset',
schemaTemplate: (setting: object) => {
return {
presets: [setting],
};
},
},
{
label: 'default',
schemaTemplate: (setting: object) => {
return {
default: setting,
};
},
},
];

for (const test of tests) {
describe(`${test.label} settings`, () => {
it(`completes ${test.label} setting property when settings exist`, async () => {
const schema = {
settings: [{ id: 'custom-setting' }, { id: 'fake-setting' }, {}],
...test.schemaTemplate({
settings: {
'█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'sections/section.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items.map((item) => item.label)).to.include.members([
`"custom-setting"`,
`"fake-setting"`,
]);
});

it(`offers no suggestions for ${test.label} setting property when there are no settings`, async () => {
const schema = {
...test.schemaTemplate({
settings: {
'█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'sections/section.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(0);
expect(completions.items.map((item) => item.label)).to.include.members([]);
});

it(`offers ${test.label} setting completion with docs from setting.label`, async () => {
const schema = {
settings: [
{ id: 'custom-setting', label: 'Custom Setting' },
{ id: 'fake-setting', label: 't:general.fake' },
{},
],
...test.schemaTemplate({
settings: {
'█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'sections/section.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
documentation: { kind: 'markdown', value: 'Custom Setting' },
}),
expect.objectContaining({
documentation: { kind: 'markdown', value: 'Fake Setting' },
}),
]),
);
});
});
}

describe('block file with valid schema', () => {
it(`offers no suggestions for default setting property when file is block`, async () => {
const source = `
{% schema %}
{
"settings": [
{"id": "custom-setting"},
{"id": "fake-setting"},
{},
],
"default": {
"settings": {
"█": ""
}
}
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(0);
expect(completions.items.map((item) => item.label)).to.include.members([]);
});
});
});

describe('invalid schema', () => {
it('completes preset setting property when settings exist and schema has small errors', async () => {
const source = `
{% schema %}
{
"settings": [
{"id": "custom-setting"},
{"id": "fake-setting"},
{},
],
"presets": [{
"settings": {
"█"
}
}]
}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const completions = await jsonLanguageService.completions(params);

assert(isCompletionList(completions));
expect(completions.items).to.have.lengthOf(2);
expect(completions.items.map((item) => item.label)).to.include.members([
`"custom-setting"`,
`"fake-setting"`,
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -3,15 +3,16 @@ import { isError, SourceCodeType } from '@shopify/theme-check-common';
import { JSONPath } from 'vscode-json-languageservice';
import { JSONCompletionItem } from 'vscode-json-languageservice/lib/umd/jsonContributions';
import { RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isBlockFile, isSectionFile } from '../../utils';
import { JSONCompletionProvider } from '../JSONCompletionProvider';
import { isSectionOrBlockSchema } from './BlockTypeCompletionProvider';
import { CompletionItemKind } from 'vscode-languageserver-protocol';
import { GetTranslationsForURI, renderTranslation, translationValue } from '../../../translations';

/**
* The PresetsSettingsPropertyCompletionProvider offers property completions of the
* `presets.[].settings.[]` objects inside section and theme block `{% schema %}` tags.
* The SettingsPropertyCompletionProvider offers property completions for:
* - `presets.[].settings.[]` objects inside `{% schema %}` tag in sections and blocks
* - `default.settings` object inside `{% schema %}` tag in sections
*
* @example
* {% schema %}
@@ -22,20 +23,29 @@ import { GetTranslationsForURI, renderTranslation, translationValue } from '../.
* { "█" },
* ]
* },
* ]
* ],
* "default": {
* "settings": {
* "█"
* }
* }
* }
* {% endschema %}
*/
export class PresetsSettingsPropertyCompletionProvider implements JSONCompletionProvider {
private uriPatterns = [/^.*\/(sections|blocks)\/[^\/]*\.liquid$/];

export class SettingsPropertyCompletionProvider implements JSONCompletionProvider {
constructor(public getDefaultSchemaTranslations: GetTranslationsForURI) {}

async completeProperty(context: RequestContext, path: JSONPath): Promise<JSONCompletionItem[]> {
if (context.doc.type !== SourceCodeType.LiquidHtml) return [];

// section files can have schemas with `presets` and `default`
// block files can have schemas with `presets` only
if (
!fileMatch(context.doc.uri, this.uriPatterns) ||
context.doc.type !== SourceCodeType.LiquidHtml ||
!isPresetSettingsPath(path)
!(
isSectionFile(context.doc.uri) &&
(isPresetSettingsPath(path) || isDefaultSettingsPath(path))
) &&
!(isBlockFile(context.doc.uri) && isPresetSettingsPath(path))
) {
return [];
}
@@ -100,5 +110,9 @@ export class PresetsSettingsPropertyCompletionProvider implements JSONCompletion
}

function isPresetSettingsPath(path: JSONPath) {
return path.at(0) === 'presets' && path.at(2) === 'settings';
return path.length === 3 && path.at(0) === 'presets' && path.at(2) === 'settings';
}

function isDefaultSettingsPath(path: JSONPath) {
return path.length === 2 && path.at(0) === 'default' && path.at(1) === 'settings';
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,18 +2,16 @@ import { deepGet } from '@shopify/theme-check-common';
import { JSONPath, MarkedString } from 'vscode-json-languageservice';
import { GetTranslationsForURI, renderTranslation, translationValue } from '../../../translations';
import { isLiquidRequestContext, LiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isSectionOrBlockFile } from '../../utils';
import { JSONHoverProvider } from '../JSONHoverProvider';

export class SchemaTranslationHoverProvider implements JSONHoverProvider {
private uriPatterns = [/(sections|blocks)\/[^\/]*\.liquid$/];

constructor(private getDefaultSchemaTranslations: GetTranslationsForURI) {}

canHover(context: RequestContext, path: JSONPath): context is LiquidRequestContext {
const label = deepGet(context.parsed, path);
return (
fileMatch(context.doc.uri, this.uriPatterns) &&
isSectionOrBlockFile(context.doc.uri) &&
isLiquidRequestContext(context) &&
path.length !== 0 &&
label &&
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, beforeEach, it, expect } from 'vitest';
import { DocumentManager } from '../../../documents';
import { JSONLanguageService } from '../../JSONLanguageService';
import { getRequestParams, mockJSONLanguageService } from '../../test/test-helpers';

describe('Module: SettingsHoverProvider', async () => {
const rootUri = 'file:///root/';
let jsonLanguageService: JSONLanguageService;
let documentManager: DocumentManager;

beforeEach(async () => {
documentManager = new DocumentManager(
undefined, // don't need a fs
undefined, // don't need a connection
undefined, // don't need client capabilities
async () => 'theme', // 'theme' mode
async () => false, // schema is invalid for tests
);
jsonLanguageService = mockJSONLanguageService(
rootUri,
documentManager,
async (uri: string) => ({
general: {
fake: 'Fake Setting',
},
}),
);

await jsonLanguageService.setup({
textDocument: {
hover: {},
completion: {
contextSupport: true,
completionItem: {
snippetSupport: true,
commitCharactersSupport: true,
documentationFormat: ['markdown'],
deprecatedSupport: true,
preselectSupport: true,
},
},
},
});
});

const tests = [
{
label: 'preset',
schemaTemplate: (setting: object) => {
return {
presets: [setting],
};
},
},
{
label: 'default',
schemaTemplate: (setting: object) => {
return {
default: setting,
};
},
},
];

for (const test of tests) {
describe(`${test.label} settings`, () => {
it(`provide hover for ${test.label} setting when setting label contains a translation key`, async () => {
const schema = {
settings: [
{ id: 'custom-setting', label: 'Custom Setting' },
{ id: 'fake-setting', label: 't:general.fake' },
],
...test.schemaTemplate({
settings: {
'fake-setting█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const hover = await jsonLanguageService.hover(params);

expect(hover).not.toBeNull();
expect(hover!.contents).to.have.lengthOf(1);
expect(hover!.contents).toEqual(['Fake Setting']);
});

it(`provide hover for ${test.label} setting when setting label is literal text`, async () => {
const schema = {
settings: [
{ id: 'custom-setting', label: 'Custom Setting' },
{ id: 'fake-setting', label: 't:general.fake' },
],
...test.schemaTemplate({
settings: {
'custom-setting█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const hover = await jsonLanguageService.hover(params);

expect(hover).not.toBeNull();
expect(hover!.contents).to.have.lengthOf(1);
expect(hover!.contents).toEqual(['Custom Setting']);
});

it(`provide no hover for ${test.label} setting when setting label does not exist`, async () => {
const schema = {
settings: [{ id: 'custom-setting' }, { id: 'fake-setting', label: 't:general.fake' }],
...test.schemaTemplate({
settings: {
'custom-setting█': '',
},
}),
};
const source = `
{% schema %}
${JSON.stringify(schema)}
{% endschema %}
`;
const params = getRequestParams(documentManager, 'blocks/block.liquid', source);
const hover = await jsonLanguageService.hover(params);

expect(hover).not.toBeNull();
expect(hover!.contents).to.have.lengthOf(0);
});
});
}
});
Original file line number Diff line number Diff line change
@@ -2,30 +2,28 @@ import { isError } from '@shopify/theme-check-common';
import { JSONPath, MarkedString } from 'vscode-json-languageservice';
import { GetTranslationsForURI, renderTranslation, translationValue } from '../../../translations';
import { isLiquidRequestContext, LiquidRequestContext, RequestContext } from '../../RequestContext';
import { fileMatch } from '../../utils';
import { isSectionOrBlockFile } from '../../utils';
import { JSONHoverProvider } from '../JSONHoverProvider';
import { AugmentedLiquidSourceCode } from '../../../documents';
import { isSectionOrBlockSchema } from '../../completions/providers/BlockTypeCompletionProvider';

export class PresetsSettingsHoverProvider implements JSONHoverProvider {
private uriPatterns = [/(sections|blocks)\/[^\/]*\.liquid$/];

export class SettingsHoverProvider implements JSONHoverProvider {
constructor(private getDefaultSchemaTranslations: GetTranslationsForURI) {}

canHover(context: RequestContext, path: JSONPath): context is LiquidRequestContext {
return (
fileMatch(context.doc.uri, this.uriPatterns) &&
isSectionOrBlockFile(context.doc.uri) &&
isLiquidRequestContext(context) &&
path.length !== 0 &&
isPresetSettingsPath(path)
(isPresetSettingsPath(path) || isDefaultSettingsPath(path))
);
}

async hover(context: RequestContext, path: JSONPath): Promise<MarkedString[]> {
if (!this.canHover(context, path)) return [];

const { doc } = context;
const label = await getSettingsLabel(doc, path.at(3) as string);
const label = await getSettingsLabel(doc, path.at(-1) as string);

if (!label) return [];

@@ -46,13 +44,21 @@ export class PresetsSettingsHoverProvider implements JSONHoverProvider {
function isPresetSettingsPath(path: JSONPath) {
return (
path.at(0) === 'presets' &&
path.at(1) === 0 &&
path.at(2) === 'settings' &&
path.at(3) !== undefined &&
typeof path.at(3) === 'string'
);
}

function isDefaultSettingsPath(path: JSONPath) {
return (
path.at(0) === 'default' &&
path.at(1) === 'settings' &&
path.at(2) !== undefined &&
typeof path.at(2) === 'string'
);
}

async function getSettingsLabel(
doc: AugmentedLiquidSourceCode,
label: string,
12 changes: 12 additions & 0 deletions packages/theme-language-server-common/src/json/utils.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,18 @@ export function fileMatch(uri: string, patterns: RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(uri));
}

export function isSectionFile(uri: string): boolean {
return /\/sections\/[^/]*\.liquid$/.test(uri);
}

export function isBlockFile(uri: string): boolean {
return /\/blocks\/[^/]*\.liquid$/.test(uri);
}

export function isSectionOrBlockFile(uri: string): boolean {
return isSectionFile(uri) || isBlockFile(uri);
}

export function findSchemaNode(ast: LiquidHtmlNode): LiquidRawTag | undefined {
const nodes = visit(ast, {
LiquidRawTag(node) {
74 changes: 74 additions & 0 deletions packages/theme-language-server-common/src/liquidDoc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect, it } from 'vitest';
import { LiquidHtmlNode } from '@shopify/theme-check-common';
import { toSourceCode } from '@shopify/theme-check-common';
import { describe } from 'vitest';
import { getSnippetDefinition } from './liquidDoc';

describe('Unit: makeGetLiquidDocDefinitions', () => {
function toAST(code: string) {
return toSourceCode('/tmp/foo.liquid', code).ast as LiquidHtmlNode;
}

it('should return default snippet definition if no renderable content is present', async () => {
const ast = toAST(`
{% doc %}
just a description
@undefined asdf
{% enddoc %}
`);

const result = getSnippetDefinition(ast, 'product-card');
expect(result).to.deep.equal({
name: 'product-card',
liquidDoc: {
parameters: [],
},
});
});

it('should extract name, description and type from param annotations', async () => {
const ast = toAST(`
{% doc %}
@param {String} firstParam - The first param
@param {Number} secondParam - The second param
@param paramWithNoType - param with no type
@param paramWithOnlyName
@param {Number} paramWithNoDescription
{% enddoc %}
`);

const result = getSnippetDefinition(ast, 'product-card');
expect(result).to.deep.equal({
name: 'product-card',
liquidDoc: {
parameters: [
{
name: 'firstParam',
description: 'The first param',
type: 'String',
},
{
name: 'secondParam',
description: 'The second param',
type: 'Number',
},
{
name: 'paramWithNoType',
description: 'param with no type',
type: null,
},
{
name: 'paramWithOnlyName',
description: null,
type: null,
},
{
name: 'paramWithNoDescription',
description: null,
type: 'Number',
},
],
},
});
});
});
50 changes: 50 additions & 0 deletions packages/theme-language-server-common/src/liquidDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SourceCodeType, visit } from '@shopify/theme-check-common';

import { LiquidHtmlNode } from '@shopify/theme-check-common';

import { LiquidDocParamNode } from '@shopify/liquid-html-parser';

export type GetSnippetDefinitionForURI = (
uri: string,
snippetName: string,
) => Promise<SnippetDefinition | undefined>;

export type SnippetDefinition = {
name: string;
liquidDoc?: LiquidDocDefinition;
};

type LiquidDocDefinition = {
parameters?: LiquidDocParameter[];
};

export type LiquidDocParameter = {
name: string;
description: string | null;
type: string | null;
};

export function getSnippetDefinition(
snippet: LiquidHtmlNode,
snippetName: string,
): SnippetDefinition {
const parameters: LiquidDocParameter[] = visit<SourceCodeType.LiquidHtml, LiquidDocParameter>(
snippet,
{
LiquidDocParamNode(node: LiquidDocParamNode) {
return {
name: node.paramName.value,
description: node.paramDescription?.value ?? null,
type: node.paramType?.value ?? null,
};
},
},
);

return {
name: snippetName,
liquidDoc: {
parameters,
},
};
}
17 changes: 17 additions & 0 deletions packages/theme-language-server-common/src/server/startServer.ts
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ import { snippetName } from '../utils/uri';
import { VERSION } from '../version';
import { CachedFileSystem } from './CachedFileSystem';
import { Configuration } from './Configuration';
import { SnippetDefinition } from '../liquidDoc';

const defaultLogger = () => {};

@@ -171,6 +172,21 @@ export function startServer(
return getDefaultSchemaTranslations();
};

const getSnippetDefinitionForURI = async (
uri: string,
snippetName: string,
): Promise<SnippetDefinition | undefined> => {
const rootUri = await findThemeRootURI(uri);
const snippetURI = path.join(rootUri, 'snippets', `${snippetName}.liquid`);
const snippet = documentManager.get(snippetURI);

if (!snippet || snippet.type !== SourceCodeType.LiquidHtml || isError(snippet.ast)) {
return undefined;
}

return snippet.getLiquidDoc(snippetName);
};

const snippetFilter = ([uri]: FileTuple) => /\.liquid$/.test(uri) && /snippets/.test(uri);
const getSnippetNamesForURI: GetSnippetNamesForURI = async (uri: string) => {
const rootUri = await findThemeRootURI(uri);
@@ -252,6 +268,7 @@ export function startServer(
getMetafieldDefinitions,
getTranslationsForURI,
getThemeSettingsSchemaForURI,
getSnippetDefinitionForURI,
);

const executeCommandProvider = new ExecuteCommandProvider(
23 changes: 23 additions & 0 deletions packages/theme-language-server-node/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# @shopify/theme-language-server-node

## 2.6.1

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-language-server-common@2.6.1
- @shopify/theme-check-docs-updater@3.7.1
- @shopify/theme-check-node@3.7.1

## 2.6.0

### Patch Changes

- Updated dependencies [c85a6131]
- Updated dependencies [931dc9b9]
- Updated dependencies [02b8967f]
- Updated dependencies [9765bece]
- Updated dependencies [dd0cd4d2]
- @shopify/theme-language-server-common@2.6.0
- @shopify/theme-check-node@3.7.0
- @shopify/theme-check-docs-updater@3.7.0

## 2.5.0

### Patch Changes
12 changes: 6 additions & 6 deletions packages/theme-language-server-node/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@shopify/theme-language-server-node",
"version": "2.5.0",
"version": "2.6.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "CP Clermont <cp.clermont@shopify.com>",
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/theme-language-server-node#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/theme-language-server-node"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"license": "MIT",
"publishConfig": {
@@ -27,9 +27,9 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@shopify/theme-language-server-common": "2.5.0",
"@shopify/theme-check-node": "^3.6.1",
"@shopify/theme-check-docs-updater": "^3.6.1",
"@shopify/theme-language-server-common": "2.6.1",
"@shopify/theme-check-node": "^3.7.1",
"@shopify/theme-check-docs-updater": "^3.7.1",
"glob": "^8.0.3",
"node-fetch": "^2.6.11",
"vscode-languageserver": "^8.0.2",
30 changes: 19 additions & 11 deletions packages/theme-language-server-node/src/metafieldDefinitions.ts
Original file line number Diff line number Diff line change
@@ -25,17 +25,25 @@ export async function fetchMetafieldDefinitionsForURI(uri: string) {
}
}

// eslint-disable-next-line no-unused-vars
async function getShopifyCliPath() {
if (isWin) {
const { stdout } = await exec(`where.exe shopify`);
const executables = stdout
.replace(/\r/g, '')
.split('\n')
.filter((exe) => exe.endsWith('bat'));
return executables.length > 0 ? executables[0] : '';
} else {
const { stdout } = await exec(`which shopify`);
return stdout.split('\n')[0].replace('\r', '');
if (process.env.NODE_ENV === 'test') {
return;
}

try {
if (isWin) {
const { stdout } = await exec(`where.exe shopify`);
const executables = stdout
.replace(/\r/g, '')
.split('\n')
.filter((exe) => exe.endsWith('bat'));
return executables.length > 0 ? executables[0] : '';
} else {
const { stdout } = await exec(`which shopify`);
return stdout.split('\n')[0].replace('\r', '');
}
} catch (_) {
// If any errors occur while trying to find the CLI, we will silently return
return;
}
}
32 changes: 32 additions & 0 deletions packages/vscode-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
## theme-check-vscode

## 3.5.3

### Patch Changes

- 841ca6d1: Update repository URL for all packages to be case sensitive
- Updated dependencies [841ca6d1]
- @shopify/theme-language-server-browser@2.6.1
- @shopify/theme-language-server-node@2.6.1
- @shopify/prettier-plugin-liquid@1.7.2
- @shopify/liquid-html-parser@2.3.2
- @shopify/theme-check-common@3.7.1

## 3.5.2

### Patch Changes

- Patch bump because it depends on:
- @shopify/liquid-html-parser
- @shopify/theme-language-server-browser
- @shopify/theme-language-server-node
- @shopify/theme-check-common
- Updated dependencies
- Updated dependencies [c85a6131]
- Updated dependencies [913d5386]
- Updated dependencies [931dc9b9]
- Updated dependencies [dd0cd4d2]
- @shopify/prettier-plugin-liquid@1.7.1
- @shopify/theme-check-common@3.7.0
- @shopify/liquid-html-parser@2.3.1
- @shopify/theme-language-server-browser@2.6.0
- @shopify/theme-language-server-node@2.6.0

## 3.5.1

### Patch Changes
18 changes: 9 additions & 9 deletions packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
@@ -4,13 +4,13 @@
"homepage": "https://github.com/Shopify/theme-tools/tree/main/packages/vscode-extension#readme",
"repository": {
"type": "git",
"url": "https://github.com/shopify/theme-tools.git",
"url": "https://github.com/Shopify/theme-tools.git",
"directory": "packages/vscode-extension"
},
"bugs": {
"url": "https://github.com/shopify/theme-tools/issues"
"url": "https://github.com/Shopify/theme-tools/issues"
},
"version": "3.5.1",
"version": "3.5.3",
"publisher": "Shopify",
"private": true,
"license": "SEE LICENSE IN LICENSE.md",
@@ -59,17 +59,17 @@
"vscode": "^1.85.0"
},
"dependencies": {
"@shopify/liquid-html-parser": "^2.3.0",
"@shopify/prettier-plugin-liquid": "^1.7.0",
"@shopify/theme-language-server-browser": "^2.5.0",
"@shopify/theme-language-server-node": "^2.5.0",
"@shopify/theme-check-common": "^3.6.1",
"@shopify/liquid-html-parser": "^2.3.2",
"@shopify/prettier-plugin-liquid": "^1.7.2",
"@shopify/theme-language-server-browser": "^2.6.1",
"@shopify/theme-language-server-node": "^2.6.1",
"@shopify/theme-check-common": "^3.7.1",
"prettier": "^2.6.2",
"vscode-languageclient": "^8.1.0",
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@shopify/theme-check-docs-updater": "^3.6.1",
"@shopify/theme-check-docs-updater": "^3.7.1",
"@types/glob": "^8.0.0",
"@types/node": "16.x",
"@types/prettier": "^2.4.2",