From ee26e9e10e17ac37daed2549e27b958d129a51ad Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 09:56:02 +0800 Subject: [PATCH 1/7] feat(theme-language-server-common): add disable-check 'this line' code action --- .../src/codeActions/CodeActionsProvider.ts | 15 ++- .../providers/DisableCheckProvider.spec.ts | 93 +++++++++++++++++++ .../providers/DisableCheckProvider.ts | 58 ++++++++++++ .../src/codeActions/providers/index.ts | 1 + .../src/codeActions/providers/utils.ts | 24 ++++- 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts create mode 100644 packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts diff --git a/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts b/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts index fe05cf119..ab1940668 100644 --- a/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts @@ -1,11 +1,21 @@ import { CodeAction, CodeActionParams, Command } from 'vscode-languageserver'; import { DiagnosticsManager } from '../diagnostics'; import { DocumentManager } from '../documents'; -import { FixAllProvider, FixProvider, SuggestionProvider } from './providers'; +import { + DisableCheckProvider, + FixAllProvider, + FixProvider, + SuggestionProvider, +} from './providers'; import { BaseCodeActionsProvider } from './BaseCodeActionsProvider'; export const CodeActionKinds = Array.from( - new Set([FixAllProvider.kind, FixProvider.kind, SuggestionProvider.kind]), + new Set([ + FixAllProvider.kind, + FixProvider.kind, + SuggestionProvider.kind, + DisableCheckProvider.kind, + ]), ); export class CodeActionsProvider { @@ -16,6 +26,7 @@ export class CodeActionsProvider { new FixAllProvider(documentManager, diagnosticsManager), new FixProvider(documentManager, diagnosticsManager), new SuggestionProvider(documentManager, diagnosticsManager), + new DisableCheckProvider(documentManager, diagnosticsManager), ]; } diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts new file mode 100644 index 000000000..cb9142d74 --- /dev/null +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -0,0 +1,93 @@ +import { Offense, Severity, SourceCodeType, path } from '@shopify/theme-check-common'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; +import { DiagnosticsManager } from '../../diagnostics'; +import { DocumentManager } from '../../documents'; +import { DisableCheckProvider } from './DisableCheckProvider'; + +describe('Unit: DisableCheckProvider', () => { + const uri = path.normalize(URI.file('/path/to/file.liquid')); + const contents = ` + {% assign x = 1 %} + + + `; + const version = 0; + const document = TextDocument.create(uri, 'liquid', version, contents); + let documentManager: DocumentManager; + let diagnosticsManager: DiagnosticsManager; + let provider: DisableCheckProvider; + + function makeOffense( + checkName: string, + needle: string, + ): Offense { + const start = contents.indexOf(needle); + const end = start + needle.length; + return { + type: SourceCodeType.LiquidHtml, + check: checkName, + message: `${checkName} problem`, + uri: 'file:///path/to/file.liquid', + severity: Severity.ERROR, + start: { ...document.positionAt(start), index: start }, + end: { ...document.positionAt(end), index: end }, + } as Offense; + } + + function cursorAt(needle: string) { + return { + textDocument: { uri }, + range: { + start: document.positionAt(contents.indexOf(needle)), + end: document.positionAt(contents.indexOf(needle)), + }, + context: { diagnostics: [] }, + }; + } + + beforeEach(() => { + documentManager = new DocumentManager(); + diagnosticsManager = new DiagnosticsManager({ sendDiagnostics: vi.fn() } as any); + documentManager.open(uri, contents, version); + provider = new DisableCheckProvider(documentManager, diagnosticsManager); + }); + + it('offers "disable for this line" inserting a disable-next-line comment above the offense, indent-matched', () => { + diagnosticsManager.set(uri, version, [makeOffense('UnusedAssign', '{% assign x = 1 %}')]); + + const codeActions = provider.codeActions(cursorAt('assign x = 1')); + const action = codeActions.find((a) => a.title === 'Disable UnusedAssign for this line'); + + expect(action).toEqual({ + title: 'Disable UnusedAssign for this line', + kind: 'quickfix', + diagnostics: expect.any(Array), + isPreferred: false, + edit: { + changes: { + [uri]: [ + { + range: { + start: { line: 1, character: 0 }, + end: { line: 1, character: 0 }, + }, + newText: ' {% # theme-check-disable-next-line UnusedAssign %}\n', + }, + ], + }, + }, + }); + }); + + it('offers no actions when the cursor does not overlap any offense', () => { + diagnosticsManager.set(uri, version, [ + makeOffense('ParserBlockingScript', ''), + ]); + + const codeActions = provider.codeActions(cursorAt('assign x = 1')); + + expect(codeActions.length).toBe(0); + }); +}); diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts new file mode 100644 index 000000000..6580c27cc --- /dev/null +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts @@ -0,0 +1,58 @@ +import { SourceCodeType } from '@shopify/theme-check-common'; +import { + CodeAction, + CodeActionKind, + CodeActionParams, + Command, + TextEdit, + WorkspaceEdit, +} from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { BaseCodeActionsProvider } from '../BaseCodeActionsProvider'; +import { isInRange, toEditCodeAction } from './utils'; + +export class DisableCheckProvider extends BaseCodeActionsProvider { + static kind = CodeActionKind.QuickFix; + + codeActions(params: CodeActionParams): (Command | CodeAction)[] { + const { uri } = params.textDocument; + const document = this.documentManager.get(uri); + const diagnostics = this.diagnosticsManager.get(uri); + if (!document || !diagnostics || document.type !== SourceCodeType.LiquidHtml) return []; + + const { textDocument } = document; + const { anomalies } = diagnostics; + const start = textDocument.offsetAt(params.range.start); + const end = textDocument.offsetAt(params.range.end); + + const anomaliesUnderCursor = anomalies.filter((anomaly) => isInRange(anomaly, start, end)); + if (anomaliesUnderCursor.length === 0) return []; + + const { offense, diagnostic } = anomaliesUnderCursor[0]; + const check = offense.check; + + return [ + toEditCodeAction( + `Disable ${check} for this line`, + disableNextLineEdit(uri, textDocument, offense.start.line, check), + [diagnostic], + DisableCheckProvider.kind, + ), + ]; + } +} + +function disableNextLineEdit( + uri: string, + textDocument: TextDocument, + line: number, + check: string, +): WorkspaceEdit { + const lineText = textDocument.getText({ + start: { line, character: 0 }, + end: { line: line + 1, character: 0 }, + }); + const indent = lineText.match(/^[ \t]*/)?.[0] ?? ''; + const newText = `${indent}{% # theme-check-disable-next-line ${check} %}\n`; + return { changes: { [uri]: [TextEdit.insert({ line, character: 0 }, newText)] } }; +} diff --git a/packages/theme-language-server-common/src/codeActions/providers/index.ts b/packages/theme-language-server-common/src/codeActions/providers/index.ts index acd48a4aa..4c3399050 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/index.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/index.ts @@ -1,3 +1,4 @@ +export { DisableCheckProvider } from './DisableCheckProvider'; export { FixProvider } from './FixProvider'; export { FixAllProvider } from './FixAllProvider'; export { SuggestionProvider } from './SuggestionProvider'; diff --git a/packages/theme-language-server-common/src/codeActions/providers/utils.ts b/packages/theme-language-server-common/src/codeActions/providers/utils.ts index 3bcab93d9..778764e78 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/utils.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/utils.ts @@ -1,5 +1,11 @@ import { Offense, SourceCodeType, WithRequired } from '@shopify/theme-check-common'; -import { CodeAction, CodeActionKind, Command, Diagnostic } from 'vscode-languageserver'; +import { + CodeAction, + CodeActionKind, + Command, + Diagnostic, + WorkspaceEdit, +} from 'vscode-languageserver'; import { Anomaly } from '../../diagnostics'; // They have an awkard API for creating them, so we have this helper here @@ -17,6 +23,22 @@ export function toCodeAction( return codeAction; } +// Edit-based sibling of toCodeAction: the action carries a WorkspaceEdit +// directly instead of a server command. +export function toEditCodeAction( + title: string, + edit: WorkspaceEdit, + diagnostics: Diagnostic[], + kind: CodeActionKind, + isPreferred: boolean = false, +): CodeAction { + const codeAction = CodeAction.create(title, kind); + codeAction.edit = edit; + codeAction.diagnostics = diagnostics; + codeAction.isPreferred = isPreferred; + return codeAction; +} + /** * The range is either the selection or cursor position, an offense is in * range if the selection and offense overlap in any way. From 1f48c9b6fd5daedf3b0d246870c3d4e256655659 Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:01:11 +0800 Subject: [PATCH 2/7] refactor(theme-language-server-common): tidy disable-check export order + drop redundant test cast --- .../src/codeActions/providers/DisableCheckProvider.spec.ts | 2 +- .../src/codeActions/providers/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index cb9142d74..4e163a233 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -33,7 +33,7 @@ describe('Unit: DisableCheckProvider', () => { severity: Severity.ERROR, start: { ...document.positionAt(start), index: start }, end: { ...document.positionAt(end), index: end }, - } as Offense; + }; } function cursorAt(needle: string) { diff --git a/packages/theme-language-server-common/src/codeActions/providers/index.ts b/packages/theme-language-server-common/src/codeActions/providers/index.ts index 4c3399050..e17180e44 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/index.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/index.ts @@ -1,4 +1,4 @@ -export { DisableCheckProvider } from './DisableCheckProvider'; export { FixProvider } from './FixProvider'; export { FixAllProvider } from './FixAllProvider'; export { SuggestionProvider } from './SuggestionProvider'; +export { DisableCheckProvider } from './DisableCheckProvider'; From 989abbedc261cb4291c6a7b306ce2f0cc54f3ffe Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:02:21 +0800 Subject: [PATCH 3/7] feat(theme-language-server-common): add disable-check 'entire file' code action --- .../providers/DisableCheckProvider.spec.ts | 27 +++++++++++++++++++ .../providers/DisableCheckProvider.ts | 11 ++++++++ 2 files changed, 38 insertions(+) diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index 4e163a233..893167d37 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -81,6 +81,33 @@ describe('Unit: DisableCheckProvider', () => { }); }); + it('offers "disable for entire file" inserting a disable comment at the top of the file', () => { + diagnosticsManager.set(uri, version, [makeOffense('UnusedAssign', '{% assign x = 1 %}')]); + + const codeActions = provider.codeActions(cursorAt('assign x = 1')); + const action = codeActions.find((a) => a.title === 'Disable UnusedAssign for entire file'); + + expect(action).toEqual({ + title: 'Disable UnusedAssign for entire file', + kind: 'quickfix', + diagnostics: expect.any(Array), + isPreferred: false, + edit: { + changes: { + [uri]: [ + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + newText: '{% # theme-check-disable UnusedAssign %}\n', + }, + ], + }, + }, + }); + }); + it('offers no actions when the cursor does not overlap any offense', () => { diagnosticsManager.set(uri, version, [ makeOffense('ParserBlockingScript', ''), diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts index 6580c27cc..abc02ecf3 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts @@ -38,10 +38,21 @@ export class DisableCheckProvider extends BaseCodeActionsProvider { [diagnostic], DisableCheckProvider.kind, ), + toEditCodeAction( + `Disable ${check} for entire file`, + disableFileEdit(uri, check), + [diagnostic], + DisableCheckProvider.kind, + ), ]; } } +function disableFileEdit(uri: string, check: string): WorkspaceEdit { + const newText = `{% # theme-check-disable ${check} %}\n`; + return { changes: { [uri]: [TextEdit.insert({ line: 0, character: 0 }, newText)] } }; +} + function disableNextLineEdit( uri: string, textDocument: TextDocument, From 3b54dcb797ca14f8f0e45a36f33c6002b2d651a8 Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:06:24 +0800 Subject: [PATCH 4/7] feat(theme-language-server-common): group disable-check actions by check name --- .../providers/DisableCheckProvider.spec.ts | 45 ++++++++++++++++++ .../providers/DisableCheckProvider.ts | 47 ++++++++++++------- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index 893167d37..dc76e4a24 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -47,6 +47,17 @@ describe('Unit: DisableCheckProvider', () => { }; } + function rangeFromTo(startNeedle: string, endNeedle: string) { + return { + textDocument: { uri }, + range: { + start: document.positionAt(contents.indexOf(startNeedle)), + end: document.positionAt(contents.indexOf(endNeedle) + endNeedle.length), + }, + context: { diagnostics: [] }, + }; + } + beforeEach(() => { documentManager = new DocumentManager(); diagnosticsManager = new DiagnosticsManager({ sendDiagnostics: vi.fn() } as any); @@ -117,4 +128,38 @@ describe('Unit: DisableCheckProvider', () => { expect(codeActions.length).toBe(0); }); + + it('emits only one action pair per check when several offenses of the same check are selected', () => { + diagnosticsManager.set(uri, version, [ + makeOffense('ParserBlockingScript', ''), + makeOffense('ParserBlockingScript', ''), + ]); + + const codeActions = provider.codeActions(rangeFromTo('2.js', '3.js')); + + expect(codeActions.length).toBe(2); + expect(codeActions.map((a) => a.title)).toEqual([ + 'Disable ParserBlockingScript for this line', + 'Disable ParserBlockingScript for entire file', + ]); + }); + + it('emits one action pair per distinct check when multiple checks are selected', () => { + diagnosticsManager.set(uri, version, [ + makeOffense('UnusedAssign', '{% assign x = 1 %}'), + makeOffense('ParserBlockingScript', ''), + ]); + + const codeActions = provider.codeActions(rangeFromTo('assign x = 1', '2.js')); + + expect(codeActions.length).toBe(4); + expect(codeActions.map((a) => a.title).sort()).toEqual( + [ + 'Disable ParserBlockingScript for entire file', + 'Disable ParserBlockingScript for this line', + 'Disable UnusedAssign for entire file', + 'Disable UnusedAssign for this line', + ].sort(), + ); + }); }); diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts index abc02ecf3..930d01b5c 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts @@ -1,9 +1,10 @@ -import { SourceCodeType } from '@shopify/theme-check-common'; +import { Offense, SourceCodeType } from '@shopify/theme-check-common'; import { CodeAction, CodeActionKind, CodeActionParams, Command, + Diagnostic, TextEdit, WorkspaceEdit, } from 'vscode-languageserver'; @@ -28,23 +29,35 @@ export class DisableCheckProvider extends BaseCodeActionsProvider { const anomaliesUnderCursor = anomalies.filter((anomaly) => isInRange(anomaly, start, end)); if (anomaliesUnderCursor.length === 0) return []; - const { offense, diagnostic } = anomaliesUnderCursor[0]; - const check = offense.check; + const byCheck = new Map(); + for (const { offense, diagnostic } of anomaliesUnderCursor) { + const existing = byCheck.get(offense.check); + if (existing) { + existing.diagnostics.push(diagnostic); + } else { + byCheck.set(offense.check, { offense, diagnostics: [diagnostic] }); + } + } - return [ - toEditCodeAction( - `Disable ${check} for this line`, - disableNextLineEdit(uri, textDocument, offense.start.line, check), - [diagnostic], - DisableCheckProvider.kind, - ), - toEditCodeAction( - `Disable ${check} for entire file`, - disableFileEdit(uri, check), - [diagnostic], - DisableCheckProvider.kind, - ), - ]; + const actions: CodeAction[] = []; + for (const [check, { offense, diagnostics: groupDiagnostics }] of byCheck) { + actions.push( + toEditCodeAction( + `Disable ${check} for this line`, + disableNextLineEdit(uri, textDocument, offense.start.line, check), + groupDiagnostics, + DisableCheckProvider.kind, + ), + toEditCodeAction( + `Disable ${check} for entire file`, + disableFileEdit(uri, check), + groupDiagnostics, + DisableCheckProvider.kind, + ), + ); + } + + return actions; } } From bc5cb2861ed544f7243650bd935ceeb7b854ea9b Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:12:40 +0800 Subject: [PATCH 5/7] feat(theme-language-server-common): skip disable-next-line action inside {% liquid %} blocks --- .../providers/DisableCheckProvider.spec.ts | 48 +++++++++++++++++++ .../providers/DisableCheckProvider.ts | 27 ++++++++--- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index dc76e4a24..504f012e2 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -163,3 +163,51 @@ describe('Unit: DisableCheckProvider', () => { ); }); }); + +describe('Unit: DisableCheckProvider — {% liquid %} blocks', () => { + const uri = path.normalize(URI.file('/path/to/file.liquid')); + const contents = `{% liquid + assign x = 1 +%}`; + const version = 0; + const document = TextDocument.create(uri, 'liquid', version, contents); + let documentManager: DocumentManager; + let diagnosticsManager: DiagnosticsManager; + let provider: DisableCheckProvider; + + function makeOffense(checkName: string, needle: string): Offense { + const start = contents.indexOf(needle); + const end = start + needle.length; + return { + type: SourceCodeType.LiquidHtml, + check: checkName, + message: `${checkName} problem`, + uri: 'file:///path/to/file.liquid', + severity: Severity.ERROR, + start: { ...document.positionAt(start), index: start }, + end: { ...document.positionAt(end), index: end }, + }; + } + + beforeEach(() => { + documentManager = new DocumentManager(); + diagnosticsManager = new DiagnosticsManager({ sendDiagnostics: vi.fn() } as any); + documentManager.open(uri, contents, version); + provider = new DisableCheckProvider(documentManager, diagnosticsManager); + }); + + it('does not offer "this line" inside a {% liquid %} block, but still offers "entire file"', () => { + diagnosticsManager.set(uri, version, [makeOffense('UnusedAssign', 'assign x = 1')]); + + const codeActions = provider.codeActions({ + textDocument: { uri }, + range: { + start: document.positionAt(contents.indexOf('assign x = 1')), + end: document.positionAt(contents.indexOf('assign x = 1')), + }, + context: { diagnostics: [] }, + }); + + expect(codeActions.map((a) => a.title)).toEqual(['Disable UnusedAssign for entire file']); + }); +}); diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts index 930d01b5c..e0dd8cf74 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts @@ -1,4 +1,5 @@ -import { Offense, SourceCodeType } from '@shopify/theme-check-common'; +import { Offense, SourceCodeType, findCurrentNode, isError } from '@shopify/theme-check-common'; +import { LiquidHtmlNode, NodeTypes } from '@shopify/liquid-html-parser'; import { CodeAction, CodeActionKind, @@ -41,13 +42,17 @@ export class DisableCheckProvider extends BaseCodeActionsProvider { const actions: CodeAction[] = []; for (const [check, { offense, diagnostics: groupDiagnostics }] of byCheck) { + if (!isInsideLiquidTag(document.ast, offense.start.index)) { + actions.push( + toEditCodeAction( + `Disable ${check} for this line`, + disableNextLineEdit(uri, textDocument, offense.start.line, check), + groupDiagnostics, + DisableCheckProvider.kind, + ), + ); + } actions.push( - toEditCodeAction( - `Disable ${check} for this line`, - disableNextLineEdit(uri, textDocument, offense.start.line, check), - groupDiagnostics, - DisableCheckProvider.kind, - ), toEditCodeAction( `Disable ${check} for entire file`, disableFileEdit(uri, check), @@ -80,3 +85,11 @@ function disableNextLineEdit( const newText = `${indent}{% # theme-check-disable-next-line ${check} %}\n`; return { changes: { [uri]: [TextEdit.insert({ line, character: 0 }, newText)] } }; } + +function isInsideLiquidTag(ast: LiquidHtmlNode | Error, index: number): boolean { + if (isError(ast)) return false; + const [currentNode, ancestors] = findCurrentNode(ast, index); + return [currentNode, ...ancestors].some( + (node) => node.type === NodeTypes.LiquidTag && node.name === 'liquid', + ); +} From 178c3bcf7d6fefd4518cbdb88cd673d19edc9348 Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:19:54 +0800 Subject: [PATCH 6/7] refactor(theme-language-server-common): use NamedTags.liquid + clearer helper name, add outside-liquid test --- .../providers/DisableCheckProvider.spec.ts | 21 ++++++++++++++++++- .../providers/DisableCheckProvider.ts | 8 +++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index 504f012e2..d83411e90 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -168,7 +168,8 @@ describe('Unit: DisableCheckProvider — {% liquid %} blocks', () => { const uri = path.normalize(URI.file('/path/to/file.liquid')); const contents = `{% liquid assign x = 1 -%}`; +%} +{% assign y = 2 %}`; const version = 0; const document = TextDocument.create(uri, 'liquid', version, contents); let documentManager: DocumentManager; @@ -210,4 +211,22 @@ describe('Unit: DisableCheckProvider — {% liquid %} blocks', () => { expect(codeActions.map((a) => a.title)).toEqual(['Disable UnusedAssign for entire file']); }); + + it('still offers "this line" for an offense outside the {% liquid %} block in the same file', () => { + diagnosticsManager.set(uri, version, [makeOffense('UnusedAssign', 'assign y = 2')]); + + const codeActions = provider.codeActions({ + textDocument: { uri }, + range: { + start: document.positionAt(contents.indexOf('assign y = 2')), + end: document.positionAt(contents.indexOf('assign y = 2')), + }, + context: { diagnostics: [] }, + }); + + expect(codeActions.map((a) => a.title)).toEqual([ + 'Disable UnusedAssign for this line', + 'Disable UnusedAssign for entire file', + ]); + }); }); diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts index e0dd8cf74..c260cfc37 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.ts @@ -1,5 +1,5 @@ import { Offense, SourceCodeType, findCurrentNode, isError } from '@shopify/theme-check-common'; -import { LiquidHtmlNode, NodeTypes } from '@shopify/liquid-html-parser'; +import { LiquidHtmlNode, NamedTags, NodeTypes } from '@shopify/liquid-html-parser'; import { CodeAction, CodeActionKind, @@ -42,7 +42,7 @@ export class DisableCheckProvider extends BaseCodeActionsProvider { const actions: CodeAction[] = []; for (const [check, { offense, diagnostics: groupDiagnostics }] of byCheck) { - if (!isInsideLiquidTag(document.ast, offense.start.index)) { + if (!isInsideLiquidLiquidTag(document.ast, offense.start.index)) { actions.push( toEditCodeAction( `Disable ${check} for this line`, @@ -86,10 +86,10 @@ function disableNextLineEdit( return { changes: { [uri]: [TextEdit.insert({ line, character: 0 }, newText)] } }; } -function isInsideLiquidTag(ast: LiquidHtmlNode | Error, index: number): boolean { +function isInsideLiquidLiquidTag(ast: LiquidHtmlNode | Error, index: number): boolean { if (isError(ast)) return false; const [currentNode, ancestors] = findCurrentNode(ast, index); return [currentNode, ...ancestors].some( - (node) => node.type === NodeTypes.LiquidTag && node.name === 'liquid', + (node) => node.type === NodeTypes.LiquidTag && node.name === NamedTags.liquid, ); } From 5151a34fc2345c69671470b22020556a3fb8f608 Mon Sep 17 00:00:00 2001 From: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com> Date: Fri, 29 May 2026 10:21:02 +0800 Subject: [PATCH 7/7] chore(theme-language-server-common): add changeset + prettier formatting for disable-check code action --- .changeset/add-disable-check-code-action.md | 5 +++++ .../src/codeActions/CodeActionsProvider.ts | 7 +------ .../src/codeActions/providers/DisableCheckProvider.spec.ts | 5 +---- 3 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 .changeset/add-disable-check-code-action.md diff --git a/.changeset/add-disable-check-code-action.md b/.changeset/add-disable-check-code-action.md new file mode 100644 index 000000000..440e110ff --- /dev/null +++ b/.changeset/add-disable-check-code-action.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-language-server-common': minor +--- + +Add code actions to disable a theme-check check for the current line or the entire file. When a theme-check diagnostic is under the cursor, two quick-fixes are offered that insert the corresponding `theme-check-disable-next-line` / `theme-check-disable` magic comment for you. diff --git a/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts b/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts index ab1940668..c2ceb017b 100644 --- a/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts +++ b/packages/theme-language-server-common/src/codeActions/CodeActionsProvider.ts @@ -1,12 +1,7 @@ import { CodeAction, CodeActionParams, Command } from 'vscode-languageserver'; import { DiagnosticsManager } from '../diagnostics'; import { DocumentManager } from '../documents'; -import { - DisableCheckProvider, - FixAllProvider, - FixProvider, - SuggestionProvider, -} from './providers'; +import { DisableCheckProvider, FixAllProvider, FixProvider, SuggestionProvider } from './providers'; import { BaseCodeActionsProvider } from './BaseCodeActionsProvider'; export const CodeActionKinds = Array.from( diff --git a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts index d83411e90..0460ff347 100644 --- a/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts +++ b/packages/theme-language-server-common/src/codeActions/providers/DisableCheckProvider.spec.ts @@ -19,10 +19,7 @@ describe('Unit: DisableCheckProvider', () => { let diagnosticsManager: DiagnosticsManager; let provider: DisableCheckProvider; - function makeOffense( - checkName: string, - needle: string, - ): Offense { + function makeOffense(checkName: string, needle: string): Offense { const start = contents.indexOf(needle); const end = start + needle.length; return {