Skip to content

Commit

Permalink
feat: Use code lens for tailwind preview (#9)
Browse files Browse the repository at this point in the history
We need to 

 - use a user-defined style file for tailwind.
 - apply styles to the rendered elements.

in the future.
  • Loading branch information
kdy1 authored Sep 21, 2023
1 parent faaa728 commit d76df42
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 21 deletions.
4 changes: 3 additions & 1 deletion exts/vscode-tailwind-preview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
258 changes: 239 additions & 19 deletions exts/vscode-tailwind-preview/src/extension.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<html>
<head>
<style>
${cssResult.css}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;

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;
}
Expand All @@ -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;
}
Expand All @@ -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);
},
};

Expand All @@ -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<vscode.CodeLens[]> {
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;
}
}
4 changes: 4 additions & 0 deletions exts/vscode-tailwind-preview/src/tokenizer/tagMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion exts/vscode-tailwind-preview/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"outDir": "out",
"sourceMap": true,
"strict": true,
"rootDir": "src"
"rootDir": "src",
"esModuleInterop": true
},
"exclude": ["node_modules", ".vscode-test"]
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 comment on commit d76df42

@vercel
Copy link

@vercel vercel bot commented on d76df42 Sep 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

swc-plugins – ./apps/swc-plugins

swc-plugins-git-main-dudy.vercel.app
swc-plugins.vercel.app
swc-plugins-dudy.vercel.app
plugins.swc.rs

Please sign in to comment.