Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add LLM instructions file suggestion #669

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b8590e7
wip
karreiro Dec 11, 2024
ab1894f
(wip 2) calling the language model api
karreiro Dec 11, 2024
a9d00c6
(wip 3) sidefix
karreiro Dec 11, 2024
5b6fc33
(wip 4) make sidekick a bit less ugly
karreiro Dec 11, 2024
b65037f
(wip 5) make sidekick a bit less ugly (2)
karreiro Dec 11, 2024
d1c5ad1
Move loading state to title button (#670)
frandiox Dec 12, 2024
4506991
Support multiple suggestions from a single request (#671)
frandiox Dec 12, 2024
e48c237
Bring hover back
karreiro Dec 12, 2024
3ee58bd
Support suggestions on selected code (#672)
frandiox Dec 12, 2024
5783a02
Hide suggestions on code change (#673)
frandiox Dec 12, 2024
4292a57
(wip) updated prompt
karreiro Dec 12, 2024
ebed821
(wip) prompt
karreiro Dec 12, 2024
9fe8f94
(wip) prompt 2
karreiro Dec 12, 2024
7565dca
Add LLM instructions file suggestion
madmath Dec 11, 2024
606f5c1
update llm instructions
madmath Dec 11, 2024
1e8e1c1
New llm instruction file
benjaminsehl Dec 12, 2024
f7b03ac
Add LLM instructions file suggestion
madmath Dec 11, 2024
02c2788
update llm instructions
madmath Dec 11, 2024
2005242
Use diff view for sidefix
frandiox Dec 13, 2024
f945a77
Improve the timing for removing suggestions
frandiox Dec 13, 2024
9b5b55a
Try to improve the prompt
frandiox Dec 13, 2024
b00728b
Update prompt to use tag convention
karreiro Dec 13, 2024
37989c2
Remove duplicated functions; extract file creation to a method because
karreiro Dec 13, 2024
ac395da
Fine tuning prompt
karreiro Dec 13, 2024
76a04a2
Remove unused items
karreiro Dec 13, 2024
1713459
Setup 'vitest' on 'packages/vscode-extension'
karreiro Jan 24, 2025
07126c3
Extract 'isCursor' and 'hasShopifyThemeLoaded' to 'utils.ts'
karreiro Jan 24, 2025
42e6f9f
Extract the logic for showing/hiding the 'Shopify Magic' button to 'u…
karreiro Jan 24, 2025
12a68d2
Removing 'inline completions' support as we are leaning towards 'text…
karreiro Jan 24, 2025
f2e89ec
Fix fs tests on Windows in 'utils.spec.ts'
karreiro Jan 24, 2025
3715a1f
- Extract `.cursorrules/copilot-instructions.md` files creation to 'l…
karreiro Jan 27, 2025
2a639d4
Extract 'RefactorProvider' from 'extensions'
karreiro Jan 27, 2025
4edd360
Extract, improve, and breakdown prompts in 'shopify-magic-prompts'
karreiro Jan 28, 2025
04bada9
Remove 'llm-instructions.template' and make tests run faster
karreiro Jan 28, 2025
425488f
Rename 'Sidekick' to 'Shopify Magic' everywhere
karreiro Jan 28, 2025
141ec78
RefactorProvider.ts -> ShopifyMagicCodeActionProvider.ts
karreiro Jan 28, 2025
e8c7a5b
Former llm-instructions.template and live prompt now share the same l…
karreiro Jan 28, 2025
db2c1d6
- Adjust file create + refine prompt
karreiro Jan 28, 2025
8003761
Improve Dialogs
karreiro Jan 28, 2025
85a8b5b
Add changeset
karreiro Jan 28, 2025
dd4eb4f
Improve 'isInstructionsFileUpdated' function following review suggestion
karreiro Jan 30, 2025
f458ed4
Refine prompt to avoid statements were it keeps the logic
karreiro Jan 30, 2025
9a6647a
Rename 'sideFix' to 'magicFix'
karreiro Jan 30, 2025
c8b67ad
Refine prompt to avoid statements were it keeps the logic [2]
karreiro Jan 31, 2025
6f2b83b
Refine prompt to avoid statements were it keeps the logic [3]
karreiro Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-rocks-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'theme-check-vscode': minor
---

Introduce support to Shopify Magic in the VS Code extension
36 changes: 32 additions & 4 deletions packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
"postinstall": "yarn --cwd ./syntaxes install",
"prebuild": "rimraf dist language-configuration.json",
"preinstall": "sh scripts/fetch-syntaxes.sh",
"pretest": "yarn run test:build && yarn run dev:build && yarn run lint",
"pretest": "yarn run test:build && yarn run dev:build",
"publish:vsce": "vsce publish --no-dependencies $npm_package_version",
"test": "vitest",
"test": "vitest -c \"./vitest.config.mjs\"",
"test:build": "tsc -p . --outDir out",
"test:watch": "tsc -p . -w --outDir out",
"type-check": "tsc --noEmit"
Expand All @@ -61,15 +61,15 @@
"dependencies": {
"@shopify/liquid-html-parser": "^2.4.0",
"@shopify/prettier-plugin-liquid": "^1.8.0",
"@shopify/theme-check-common": "^3.7.2",
"@shopify/theme-check-docs-updater": "^3.7.2",
"@shopify/theme-language-server-browser": "^2.7.0",
"@shopify/theme-language-server-node": "^2.7.0",
"@shopify/theme-check-common": "^3.7.2",
"prettier": "^2.6.2",
"vscode-languageclient": "^8.1.0",
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@shopify/theme-check-docs-updater": "^3.7.2",
"@types/glob": "^8.0.0",
"@types/node": "16.x",
"@types/prettier": "^2.4.2",
Expand Down Expand Up @@ -99,8 +99,36 @@
{
"command": "shopifyLiquid.runChecks",
"title": "Liquid Theme Check: Run Checks"
},
{
"command": "shopifyLiquid.magicFix",
"title": "Apply Shopify Magic suggestion"
},
{
"command": "shopifyLiquid.shopifyMagic",
"title": "✨ Shopify Magic",
"enablement": "!shopifyLiquid.shopifyMagic.isLoading"
},
{
"command": "shopifyLiquid.shopifyMagicLoading",
"title": "✨ Analyzing",
"enablement": "false"
}
],
"menus": {
"editor/title": [
{
"command": "shopifyLiquid.shopifyMagic",
"group": "navigation",
"when": "shopifyLiquid.shopifyMagic.visible"
},
{
"command": "shopifyLiquid.shopifyMagicLoading",
"group": "navigation",
"when": "shopifyLiquid.shopifyMagicLoading.visible"
}
]
},
"configuration": {
"title": "Shopify Liquid | Syntax Highlighting & Linter by Shopify",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CodeAction, CodeActionKind, CodeActionProvider, Range, TextDocument } from 'vscode';

export class ShopifyMagicCodeActionProvider implements CodeActionProvider {
public provideCodeActions(document: TextDocument, range: Range) {
const title = 'Refactor using Shopify Magic';
const kind = CodeActionKind.RefactorRewrite;
const refactorAction = new CodeAction(title, kind);

refactorAction.command = {
command: 'shopifyLiquid.shopifyMagic',
title,
arguments: [document, range],
};

return [refactorAction];
}
}
129 changes: 116 additions & 13 deletions packages/vscode-extension/src/node/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { FileStat, FileTuple, path as pathUtils } from '@shopify/theme-check-common';
import * as path from 'node:path';
import { commands, ExtensionContext, languages, Uri, workspace } from 'vscode';
import {
commands,
ExtensionContext,
languages,
TextEditor,
TextEditorDecorationType,
Uri,
workspace,
} from 'vscode';
import {
DocumentSelector,
LanguageClient,
Expand All @@ -11,20 +19,32 @@ import {
import { documentSelectors } from '../common/constants';
import LiquidFormatter from '../common/formatter';
import { vscodePrettierFormat } from './formatter';
import { getShopifyMagicAnalysis, ShopifyMagicDecoration } from './shopify-magic';
import { showShopifyMagicButton, showShopifyMagicLoadingButton } from './ui';
import { createInstructionsFiles } from './llm-instructions';
import { ShopifyMagicCodeActionProvider } from './ShopifyMagicCodeActionProvider';
import { applySuggestion, conflictMarkerStart } from './shopify-magic-suggestions';

let $client: LanguageClient | undefined;
let $editor: TextEditor | undefined;
let $decorations: TextEditorDecorationType[] = [];
let $isApplyingSuggestion = false;
let $previousShownConflicts = new Map<string, number>();

const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

let client: LanguageClient | undefined;

export async function activate(context: ExtensionContext) {
const runChecksCommand = 'themeCheck/runChecks';

await showShopifyMagicButton();
await createInstructionsFiles(context);

context.subscriptions.push(
commands.registerCommand('shopifyLiquid.restart', () => restartServer(context)),
);
context.subscriptions.push(
commands.registerCommand('shopifyLiquid.runChecks', () => {
client!.sendRequest('workspace/executeCommand', { command: runChecksCommand });
$client!.sendRequest('workspace/executeCommand', { command: runChecksCommand });
}),
);
context.subscriptions.push(
Expand All @@ -33,6 +53,65 @@ export async function activate(context: ExtensionContext) {
new LiquidFormatter(vscodePrettierFormat),
),
);
context.subscriptions.push(
languages.registerCodeActionsProvider(
[{ language: 'liquid' }],
new ShopifyMagicCodeActionProvider(),
),
);
context.subscriptions.push(
commands.registerTextEditorCommand(
'shopifyLiquid.shopifyMagic',
async (textEditor: TextEditor) => {
$editor = textEditor;
await analyse(textEditor);
},
),
);
context.subscriptions.push(
commands.registerCommand('shopifyLiquid.magicFix', async (suggestion: any) => {
$isApplyingSuggestion = true;
applySuggestion($editor, suggestion);

// Only dispose the decoration associated with this suggestion
const decorationIndex = $decorations.findIndex((d) => d.key === suggestion.key);
if (decorationIndex !== -1) {
$decorations[decorationIndex].dispose();
$decorations.splice(decorationIndex, 1);
}

$isApplyingSuggestion = false;
}),
);
context.subscriptions.push(
workspace.onDidChangeTextDocument(({ contentChanges, reason, document }) => {
// Each shown suggestion fix is displayed as a conflict in the editor. We want to
// hide all suggestion hints when the user starts typing, as they are no longer
// relevant, but we don't want to remove them on conflict resolution since that
// only means the user has accepted/rejected a suggestion and might continue with
// the other suggestions.
const currentShownConflicts = document.getText().split(conflictMarkerStart).length - 1;

if (
// Ignore when there are no content changes
contentChanges.length > 0 &&
// Ignore when initiating the diff view (it triggers a change event)
!$isApplyingSuggestion &&
// Ignore undo/redos
reason === undefined &&
// Only dispose decorations when there are no conflicts currently shown (no diff views)
// and when there were no conflicts shown previously. This means that the current
// change is not related to a conflict resolution but a manual user input.
currentShownConflicts === 0 &&
!$previousShownConflicts.get(document.fileName)
) {
disposeDecorations();
}

// Store the previous number of conflicts shown for this document.
$previousShownConflicts.set(document.fileName, currentShownConflicts);
}),
);

await startServer(context);
}
Expand All @@ -55,44 +134,44 @@ async function startServer(context: ExtensionContext) {
documentSelector: documentSelectors as DocumentSelector,
};

client = new LanguageClient(
$client = new LanguageClient(
'shopifyLiquid',
'Theme Check Language Server',
serverOptions,
clientOptions,
);

client.onRequest('fs/readDirectory', async (uriString: string): Promise<FileTuple[]> => {
$client.onRequest('fs/readDirectory', async (uriString: string): Promise<FileTuple[]> => {
const results = await workspace.fs.readDirectory(Uri.parse(uriString));
return results.map(([name, type]) => [pathUtils.join(uriString, name), type]);
});

client.onRequest('fs/readFile', async (uriString: string): Promise<string> => {
$client.onRequest('fs/readFile', async (uriString: string): Promise<string> => {
const bytes = await workspace.fs.readFile(Uri.parse(uriString));
return Buffer.from(bytes).toString('utf8');
});

client.onRequest('fs/stat', async (uriString: string): Promise<FileStat> => {
$client.onRequest('fs/stat', async (uriString: string): Promise<FileStat> => {
return workspace.fs.stat(Uri.parse(uriString));
});

client.start();
$client.start();
}

async function stopServer() {
try {
if (client) {
await Promise.race([client.stop(), sleep(1000)]);
if ($client) {
await Promise.race([$client.stop(), sleep(1000)]);
}
} catch (e) {
console.error(e);
} finally {
client = undefined;
$client = undefined;
}
}

async function restartServer(context: ExtensionContext) {
if (client) {
if ($client) {
await stopServer();
}
await startServer(context);
Expand All @@ -115,3 +194,27 @@ async function getServerOptions(context: ExtensionContext): Promise<ServerOption
},
};
}

async function analyse(textEditor: TextEditor) {
try {
await showShopifyMagicLoadingButton();
const decorations = await getShopifyMagicAnalysis(textEditor);
applyDecorations(decorations);
} finally {
await showShopifyMagicButton();
}
}

function disposeDecorations() {
$decorations.forEach((decoration) => decoration.dispose());
$decorations = [];
}

function applyDecorations(decorations: ShopifyMagicDecoration[]) {
disposeDecorations();

decorations.forEach((decoration) => {
$decorations.push(decoration.type);
$editor?.setDecorations(decoration.type, [decoration.options]);
});
}
10 changes: 10 additions & 0 deletions packages/vscode-extension/src/node/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Uri, workspace } from 'vscode';

export async function fileExists(path: string): Promise<boolean> {
try {
await workspace.fs.stat(Uri.file(path));
return true;
} catch (e) {
return false;
}
}
Loading