Skip to content

Commit

Permalink
Refactor code writing modules (onlook-dev#81)
Browse files Browse the repository at this point in the history
* Refactor code writing modules
  • Loading branch information
Kitenite authored Jul 24, 2024
1 parent d3e2974 commit 2408d72
Show file tree
Hide file tree
Showing 25 changed files with 448 additions and 342 deletions.
Binary file modified app/bun.lockb
Binary file not shown.
19 changes: 15 additions & 4 deletions app/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export enum EditorAttributes {
// DOM attributes
ONLOOK_TOOLBAR = 'onlook-toolbar',
ONLOOK_RECT_ID = 'onlook-rect',
ONLOOK_STYLESHEET_ID = 'onlook-stylesheet',

// Data attributes
DATA_ONLOOK_ID = 'data-onlook-id',
DATA_ONLOOK_IGNORE = 'data-onlook-ignore',
DATA_ONLOOK_SAVED = 'data-onlook-saved',
Expand All @@ -12,9 +14,12 @@ export enum EditorAttributes {
}

export enum WebviewChannels {
// Style
STYLE_UPDATED = 'style-updated',
UPDATE_STYLE = 'update-style',
CLEAR_STYLE_SHEET = 'clear-style-sheet',

// Mouse events
MOUSE_MOVE = 'mouse-move',
MOUSE_DOWN = 'mouse-down',
MOUSE_OVER_ELEMENT = 'hover-element',
Expand All @@ -23,12 +28,18 @@ export enum WebviewChannels {
}

export enum MainChannels {
WEBVIEW_PRELOAD_PATH = 'webview-preload-path',
OPEN_CODE_BLOCK = 'open-code-block',
WRITE_CODE_BLOCK = 'write-code-block',
GET_STYLE_CODE = 'get-style-code',
// Code
GET_CODE_BLOCK = 'get-code-block',
GET_CODE_BLOCKS = 'get-code-blocks',
GET_STYLE_CODE_DIFFS = 'get-style-code-diffs',
WRITE_CODE_BLOCKS = 'write-code-blocks',
VIEW_SOURCE_CODE = 'view-source-code',

// Tunnel
OPEN_TUNNEL = 'open-tunnel',
CLOSE_TUNNEL = 'close-tunnel',

// Analytics
ANLYTICS_PREF_SET = 'analytics-pref-set',
SEND_ANALYTICS = 'send-analytics',
}
Expand Down
2 changes: 1 addition & 1 deletion app/common/helpers/template.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { compressSync, decompressSync, strFromU8, strToU8 } from 'fflate';
import { EditorAttributes } from '../constants';
import { TemplateNode } from '../models';
import { TemplateNode } from '../models/element/templateNode';

export function getTemplateNodeFromElement(element: Element): TemplateNode | undefined {
const dataOnlookId = element.getAttribute(EditorAttributes.DATA_ONLOOK_ID);
Expand Down
44 changes: 0 additions & 44 deletions app/common/models.ts

This file was deleted.

25 changes: 25 additions & 0 deletions app/common/models/element/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// DOM
export interface DomElement {
selector: string;
rect: DOMRect;
styles: CSSStyleDeclaration;
encodedTemplateNode?: string;
parent?: ParentDomElement;
}

export interface ParentDomElement {
selector: string;
rect: DOMRect;
encodedTemplateNode?: string;
}

// Engine
export interface ElementMetadata {
selector: string;
rect: DOMRect;
parentRect: DOMRect;
computedStyle: CSSStyleDeclaration;
webviewId: string;
dataOnlookId?: string;
tagName: string;
}
17 changes: 17 additions & 0 deletions app/common/models/element/templateNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface TemplateNode {
path: string;
startTag: TemplateTag;
endTag: TemplateTag;
commit: string;
name?: string;
}

export interface TemplateTag {
start: TemplateTagPosition;
end: TemplateTagPosition;
}

export interface TemplateTagPosition {
line: number;
column: number;
}
14 changes: 14 additions & 0 deletions app/common/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TemplateNode } from './element/templateNode';

export interface StyleCodeDiff {
original: string;
generated: string;
param: StyleChangeParam;
}

export interface StyleChangeParam {
selector: string;
templateNode: TemplateNode;
tailwind: string;
codeBlock: string;
}
4 changes: 4 additions & 0 deletions app/common/models/tunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TunnelResult {
url: string;
password: string;
}
80 changes: 80 additions & 0 deletions app/electron/main/code/babel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import generate from '@babel/generator';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import t from '@babel/types';
import { twMerge } from 'tailwind-merge';
import { StyleChangeParam, StyleCodeDiff } from '/common/models';

export function getStyleCodeDiffs(styleParams: StyleChangeParam[]): StyleCodeDiff[] {
const diffs: StyleCodeDiff[] = [];
const generateOptions = { retainLines: true, compact: false };

for (const styleParam of styleParams) {
const codeBlock = styleParam.codeBlock;
const ast = parseJsx(codeBlock);
const original = removeSemiColonIfApplicable(
generate(ast, generateOptions, codeBlock).code,
codeBlock,
);

addClassToAst(ast, styleParam.tailwind);

const generated = removeSemiColonIfApplicable(
generate(ast, generateOptions, codeBlock).code,
codeBlock,
);
diffs.push({ original, generated, param: styleParam });
}

return diffs;
}

function removeSemiColonIfApplicable(code: string, original: string) {
if (!original.endsWith(';') && code.endsWith(';')) {
return code.slice(0, -1);
}
return code;
}

function parseJsx(code: string) {
return parse(code, {
plugins: ['typescript', 'jsx'],
});
}

function addClassToAst(ast: t.File, className: string) {
let processed = false;
traverse(ast, {
JSXOpeningElement(path) {
if (processed) {
return;
}
let classNameAttr = null;
path.node.attributes.forEach((attribute) => {
if (t.isJSXAttribute(attribute) && attribute.name.name === 'className') {
classNameAttr = attribute;

if (t.isStringLiteral(attribute.value)) {
attribute.value.value = twMerge(attribute.value.value, className);
}
// Handle className that is an expression (e.g., cn("class1", className))
else if (
t.isJSXExpressionContainer(attribute.value) &&
t.isCallExpression(attribute.value.expression)
) {
attribute.value.expression.arguments.push(t.stringLiteral(className));
}
}
});

if (!classNameAttr) {
const newClassNameAttr = t.jsxAttribute(
t.jsxIdentifier('className'),
t.stringLiteral(className),
);
path.node.attributes.push(newClassNameAttr);
}
processed = true;
},
});
}
114 changes: 0 additions & 114 deletions app/electron/main/code/files.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { shell } from 'electron';
import { promises as fs } from 'fs';
import * as path from 'path';
import { compareTemplateNodes } from '/common/helpers/template';
import { CodeResult, TemplateNode } from '/common/models';

export async function readFile(filePath: string): Promise<string> {
try {
Expand All @@ -24,114 +21,3 @@ export async function writeFile(filePath: string, content: string): Promise<void
throw error;
}
}

export async function readBlock(templateNode: TemplateNode): Promise<string> {
try {
const filePath = templateNode.path;

const startTag = templateNode.startTag;
const startRow = startTag.start.line;
const startColumn = startTag.start.column;

const endTag = templateNode.endTag || startTag;
const endRow = endTag.end.line;
const endColumn = endTag.end.column;

const fileContent = await readFile(filePath);
const lines = fileContent.split('\n');

const selectedText = lines
.slice(startRow - 1, endRow)
.map((line, index, array) => {
if (index === 0 && array.length === 1) {
// Only one line
return line.substring(startColumn - 1, endColumn);
} else if (index === 0) {
// First line of multiple
return line.substring(startColumn - 1);
} else if (index === array.length - 1) {
// Last line
return line.substring(0, endColumn);
}
// Full lines in between
return line;
})
.join('\n');

return selectedText;
} catch (error: any) {
console.error('Error reading range from file:', error);
throw error;
}
}

export async function writeCodeResults(codeResults: CodeResult[]): Promise<void> {
// Write from bottom to prevent line offset
const sortedCodeResults = codeResults
.sort((a, b) => compareTemplateNodes(a.param.templateNode, b.param.templateNode))
.toReversed();
const files = new Map<string, string>();

for (const result of sortedCodeResults) {
let fileContent = files.get(result.param.templateNode.path);
if (!fileContent) {
fileContent = await readFile(result.param.templateNode.path);
}

const newFileContent = await writeBlock(
result.param.templateNode,
result.generated,
fileContent,
);
files.set(result.param.templateNode.path, newFileContent);
}

for (const [filePath, content] of files) {
await writeFile(filePath, content);
}
}

export async function writeBlock(
templateNode: TemplateNode,
newBlock: string,
fileContent: string,
): Promise<string> {
try {
const startTag = templateNode.startTag;
const startRow = startTag.start.line;
const startColumn = startTag.start.column;

const endTag = templateNode.endTag || startTag;
const endRow = endTag.end.line;
const endColumn = endTag.end.column;

const lines = fileContent.split('\n');
const before = lines.slice(0, startRow - 1).join('\n');
const after = lines.slice(endRow).join('\n');

const firstLine = lines[startRow - 1].substring(0, startColumn - 1);
const lastLine = lines[endRow - 1].substring(endColumn);

const newFileContent = [before, firstLine + newBlock + lastLine, after].join('\n');
return newFileContent;
} catch (error: any) {
console.error('Error replacing range in file:', error);
throw error;
}
}

export function openInVsCode(templateNode: TemplateNode) {
const filePath = templateNode.path;
const startTag = templateNode.startTag;
const endTag = templateNode.endTag || startTag;
let command = `vscode://file/${filePath}`;

if (startTag && endTag) {
const startRow = startTag.start.line;
const startColumn = startTag.start.column;
const endRow = endTag.end.line;
const endColumn = endTag.end.column - 1;
command += `:${startRow}:${startColumn}:${endRow}:${endColumn}`;
}
shell.openExternal(command);
}
Loading

0 comments on commit 2408d72

Please sign in to comment.