From d76df4204c619ba10456efb5bf99139c30a88921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 22 Sep 2023 04:56:10 +0900 Subject: [PATCH] feat: Use code lens for tailwind preview (#9) We need to - use a user-defined style file for tailwind. - apply styles to the rendered elements. in the future. --- exts/vscode-tailwind-preview/package.json | 4 +- exts/vscode-tailwind-preview/src/extension.ts | 258 ++++++++++++++++-- .../src/tokenizer/tagMatcher.ts | 4 + exts/vscode-tailwind-preview/tsconfig.json | 3 +- pnpm-lock.yaml | 6 + 5 files changed, 254 insertions(+), 21 deletions(-) diff --git a/exts/vscode-tailwind-preview/package.json b/exts/vscode-tailwind-preview/package.json index 2e9eb7c..184787f 100644 --- a/exts/vscode-tailwind-preview/package.json +++ b/exts/vscode-tailwind-preview/package.json @@ -79,6 +79,8 @@ "typescript": "^5.2.2" }, "dependencies": { - "moo": "^0.5.2" + "moo": "^0.5.2", + "postcss": "^8.4.30", + "postcss-load-config": "^4.0.1" } } diff --git a/exts/vscode-tailwind-preview/src/extension.ts b/exts/vscode-tailwind-preview/src/extension.ts index 1d39df3..d0d54cd 100644 --- a/exts/vscode-tailwind-preview/src/extension.ts +++ b/exts/vscode-tailwind-preview/src/extension.ts @@ -1,19 +1,78 @@ import * as vscode from "vscode"; import * as path from "path"; -import { findMatchingTag, getTagForPosition } from "./tokenizer/tagMatcher"; +import postcss from "postcss"; +import postcssrc from "postcss-load-config"; +import tailwindcss from "tailwindcss"; +import { + findMatchingTag, + getTagForPosition, + getTagsForPosition, + getValidTags, +} from "./tokenizer/tagMatcher"; import { parseTags } from "./tokenizer/tagParser"; +import resolveConfig from "tailwindcss/resolveConfig"; +import { Match } from "./tokenizer/interfaces"; -const cats = { - "Coding Cat": "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif", - "Compiling Cat": "https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif", - "Testing Cat": "https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif", -}; +const COMMAND_OPEN_PREVIEW = "dudy.tailwind-preview.open"; + +/** + * + */ +async function renderTag( + document: vscode.TextDocument, + tag: Match, + styled: boolean +): Promise<[string, vscode.Range] | undefined> { + if ("jest-snapshot" !== document.languageId) { + return undefined; + } + + const range = new vscode.Range( + document.positionAt(tag.opening.start), + document.positionAt(tag.closing.end) + ); + const htmlContent = document.getText(range); + + if (!styled) { + return [htmlContent, range]; + } + + const css = postcss.parse( + ` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + { from: document.fileName } + ); + + // console.log('result', css); + const postcssConfig = await postcssrc({}, document.fileName); + + const plugins = postcssConfig.plugins; + const cssResult = await postcss(plugins).process(css); + + const finalHtml = ` + + + + + + ${htmlContent} + + `; + + return [finalHtml.trim(), range]; +} /// Check if the current text looks like a tailwind component. -function renderHtml( +async function renderHtml( document: vscode.TextDocument, - position: vscode.Position -): [string, vscode.Range] | undefined { + position: vscode.Position, + styled: boolean +): Promise<[string, vscode.Range] | undefined> { if ("jest-snapshot" !== document.languageId) { return undefined; } @@ -22,22 +81,18 @@ function renderHtml( const tags = parseTags(text); const tag = getTagForPosition(tags, document.offsetAt(position), true); - - if (tag) { - const range = new vscode.Range( - document.positionAt(tag.opening.start), - document.positionAt(tag.closing.end) - ); - return [document.getText(range), range]; + if (!tag) { + return; } - return; + + return await renderTag(document, tag, styled); } export function activate(context: vscode.ExtensionContext) { // Show image on hover const hoverProvider: vscode.HoverProvider = { async provideHover(document, position) { - const rendeded = renderHtml(document, position); + const rendeded = await renderHtml(document, position, false); if (!rendeded) { return; } @@ -57,7 +112,7 @@ export function activate(context: vscode.ExtensionContext) { path.join(context.extensionPath, "images", path.sep) ); - return new vscode.Hover(content, new vscode.Range(position, position)); + return new vscode.Hover(content, range); }, }; @@ -66,4 +121,169 @@ export function activate(context: vscode.ExtensionContext) { vscode.languages.registerHoverProvider(lang, hoverProvider) ); } + + // HoverProvider does not support styling, so we need to use a Webview. + // Instead of using a command pallette, we use a code lens to show the "open preview" button. + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + "jest-snapshot", + new PreviewCodeLensProvider() + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + COMMAND_OPEN_PREVIEW, + async (document: vscode.TextDocument, pos: vscode.Position) => { + PreviewPanel.createOrShow(context.extensionUri, document, pos); + } + ) + ); +} + +class PreviewCodeLensProvider implements vscode.CodeLensProvider { + async provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken + ): Promise { + const text = document.getText(); + const tags = getValidTags(parseTags(text)); + + const codeLenses: vscode.CodeLens[] = []; + + for (const tag of tags) { + const range = new vscode.Range( + document.positionAt(tag.opening.start), + document.positionAt(tag.closing.end) + ); + + const codeLens = new vscode.CodeLens(range, { + title: "Preview", + command: COMMAND_OPEN_PREVIEW, + arguments: [document, document.positionAt(tag.opening.start)], + }); + + codeLenses.push(codeLens); + } + + return codeLenses; + } +} + +class PreviewPanel { + /** + * Track the currently panel. Only allow a single panel to exist at a time. + */ + public static currentPanel: PreviewPanel | undefined; + + public static readonly viewType = "tailwindPreview"; + + private readonly _panel: vscode.WebviewPanel; + private readonly _extensionUri: vscode.Uri; + private _disposables: vscode.Disposable[] = []; + + public static createOrShow( + extensionUri: vscode.Uri, + document: vscode.TextDocument, + pos: vscode.Position + ) { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + + // If we already have a panel, show it. + if (PreviewPanel.currentPanel) { + PreviewPanel.currentPanel._panel.reveal(column); + return; + } + + // Otherwise, create a new panel. + const panel = vscode.window.createWebviewPanel( + PreviewPanel.viewType, + "Tailwind Preview", + column || vscode.ViewColumn.One, + {} + ); + + PreviewPanel.currentPanel = new PreviewPanel(panel, extensionUri); + PreviewPanel.currentPanel.loadNew(document, pos); + } + + public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + PreviewPanel.currentPanel = new PreviewPanel(panel, extensionUri); + } + + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + this._panel = panel; + this._extensionUri = extensionUri; + + // Set the webview's initial html content + this._update(); + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programmatically + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // Update the content based on view changes + this._panel.onDidChangeViewState( + (e) => { + if (this._panel.visible) { + this._update(); + } + }, + null, + this._disposables + ); + + // Handle messages from the webview + this._panel.webview.onDidReceiveMessage( + (message) => { + switch (message.command) { + case "alert": + vscode.window.showErrorMessage(message.text); + return; + } + }, + null, + this._disposables + ); + } + + public dispose() { + PreviewPanel.currentPanel = undefined; + + // Clean up our resources + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + if (x) { + x.dispose(); + } + } + } + + private _update() { + // TODO + } + + private async loadNew(document: vscode.TextDocument, pos: vscode.Position) { + const res = await this.getHtmlForWebview(document, pos); + if (res) { + this._panel.webview.html = res; + } + } + + private async getHtmlForWebview( + document: vscode.TextDocument, + pos: vscode.Position + ) { + const res = await renderHtml(document, pos, true); + if (!res) { + return; + } + const [htmlText] = res; + + return htmlText; + } } diff --git a/exts/vscode-tailwind-preview/src/tokenizer/tagMatcher.ts b/exts/vscode-tailwind-preview/src/tokenizer/tagMatcher.ts index de5d898..ed77b79 100644 --- a/exts/vscode-tailwind-preview/src/tokenizer/tagMatcher.ts +++ b/exts/vscode-tailwind-preview/src/tokenizer/tagMatcher.ts @@ -61,6 +61,10 @@ export function getTagsForPosition(tagsList: PartialMatch[], position: number) { ) as Match[]; } +export function getValidTags(tagsList: PartialMatch[]) { + return tagsList.filter((pair) => isTagPairValid(pair)) as Match[]; +} + export function getTagForPosition( tagsList: PartialMatch[], position: number, diff --git a/exts/vscode-tailwind-preview/tsconfig.json b/exts/vscode-tailwind-preview/tsconfig.json index 49682db..68d9ac6 100644 --- a/exts/vscode-tailwind-preview/tsconfig.json +++ b/exts/vscode-tailwind-preview/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "out", "sourceMap": true, "strict": true, - "rootDir": "src" + "rootDir": "src", + "esModuleInterop": true }, "exclude": ["node_modules", ".vscode-test"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d25afb9..512a7d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,12 @@ importers: moo: specifier: ^0.5.2 version: 0.5.2 + postcss: + specifier: ^8.4.30 + version: 8.4.30 + postcss-load-config: + specifier: ^4.0.1 + version: 4.0.1(postcss@8.4.30) devDependencies: '@types/moo': specifier: ^0.5.6