Skip to content

Commit

Permalink
Write to code handle delete (#502)
Browse files Browse the repository at this point in the history
Features:
* Delete and restore element
* Show delete warning

Refactoring:
* Clean up code diff requests

Bug fixes:
* Better editing styles
* Blurring inputs on enter
* Better browser url inputs
  • Loading branch information
Kitenite authored Oct 11, 2024
1 parent ef0b0be commit 5a9cd7d
Show file tree
Hide file tree
Showing 45 changed files with 632 additions and 316 deletions.
Binary file modified app/bun.lockb
Binary file not shown.
4 changes: 3 additions & 1 deletion app/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface StyleActionTarget extends ActionTargetWithSelector {
export interface ActionElementLocation {
position: InsertPos;
targetSelector: string;
index?: number;
index: number;
}

export interface MoveActionLocation extends ActionElementLocation {
Expand Down Expand Up @@ -48,6 +48,7 @@ export interface InsertElementAction {
element: ActionElement;
styles: Record<string, string>;
editText?: boolean;
codeBlock?: string;
}

export interface RemoveElementAction {
Expand All @@ -56,6 +57,7 @@ export interface RemoveElementAction {
location: ActionElementLocation;
element: ActionElement;
styles: Record<string, string>;
codeBlock?: string;
}

export interface MoveElementAction {
Expand Down
4 changes: 4 additions & 0 deletions app/common/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ export function timeSince(date: Date): string {
}
return Math.floor(seconds) + 's';
}

export function assertNever(n: never): never {
throw new Error(`Expected \`never\`, found: ${JSON.stringify(n)}`);
}
3 changes: 2 additions & 1 deletion app/common/models/code.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { InsertedElement, MovedElementWithTemplate } from './element/domAction';
import { InsertedElement, MovedElementWithTemplate, RemovedElement } from './element/codeAction';
import { TemplateNode } from './element/templateNode';

export interface CodeDiffRequest {
selector: string;
templateNode: TemplateNode;
insertedElements: InsertedElement[];
removedElements: RemovedElement[];
movedElements: MovedElementWithTemplate[];
attributes: Record<string, string>;
textContent?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { InsertPos } from '..';
import { TemplateNode } from './templateNode';
import { ActionElementLocation, MoveActionLocation } from '/common/actions';

export enum DomActionType {
export enum CodeActionType {
MOVE = 'move-element',
INSERT = 'insert-element',
REMOVE = 'remove-element',
}

export interface DomActionElement {
type: DomActionType;
interface BaseCodeActionElement {
type: CodeActionType;
location: ActionElementLocation;
}

Expand All @@ -18,9 +19,9 @@ export interface ActionMoveLocation extends ActionElementLocation {
index: number;
}

export interface MovedElement extends DomActionElement {
export interface MovedElement extends BaseCodeActionElement {
selector: string;
type: DomActionType.MOVE;
type: CodeActionType.MOVE;
location: MoveActionLocation;
}

Expand All @@ -33,15 +34,23 @@ export interface TextEditedElement {
content: string;
}

export interface InsertedElement extends DomActionElement {
type: DomActionType.INSERT;
export interface InsertedElement extends BaseCodeActionElement {
type: CodeActionType.INSERT;
tagName: string;
children: InsertedElement[];
attributes: Record<string, string>;
textContent?: string;
codeBlock?: string;
}

export interface RemovedElement extends BaseCodeActionElement {
type: CodeActionType.REMOVE;
codeBlock?: string;
}

export interface StyleChange {
selector: string;
styles: Record<string, string>;
}

export type CodeActionElement = MovedElement | InsertedElement | RemovedElement;
1 change: 1 addition & 0 deletions app/common/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface UserSettings {
enableAnalytics?: boolean;
ideType?: IdeType;
signInMethod?: string;
shouldWarnDelete?: boolean;
}

export interface ProjectsCache {
Expand Down
21 changes: 3 additions & 18 deletions app/electron/main/code/classes.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import { readCodeBlock } from '.';
import { parseJsx } from './helpers';
import { parseJsxCodeBlock } from './helpers';
import { TemplateNode } from '/common/models/element/templateNode';

export async function getTemplateNodeClass(templateNode: TemplateNode): Promise<string[]> {
const codeBlock = await readCodeBlock(templateNode);
const ast = parseJsx(codeBlock);
const ast = parseJsxCodeBlock(codeBlock);
if (!ast) {
return [];
}

let classes: string[] | null = null;
traverse(ast, {
JSXElement(path) {
if (!path) {
return;
}
if (classes) {
return;
}
const node = path.node;
classes = getNodeClasses(node);
path.stop();
},
});

const classes = getNodeClasses(ast);
return classes || [];
}

Expand Down
20 changes: 9 additions & 11 deletions app/electron/main/code/diff/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import generate, { GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types';
import { readFile } from '../files';
import { parseJsx, removeSemiColonIfApplicable } from '../helpers';
import { parseJsxFile, removeSemiColonIfApplicable } from '../helpers';
import { transformAst } from './transform';
import { CodeDiff, CodeDiffRequest } from '/common/models/code';
import { TemplateNode } from '/common/models/element/templateNode';
Expand All @@ -11,27 +11,25 @@ interface RequestsByPath {
codeBlock: string;
}

export async function getCodeDiffs(
templateToCodeDiff: Map<TemplateNode, CodeDiffRequest>,
): Promise<CodeDiff[]> {
const groupedRequests = await groupRequestsByTemplatePath(templateToCodeDiff);
export async function getCodeDiffs(requests: CodeDiffRequest[]): Promise<CodeDiff[]> {
const groupedRequests = await groupRequestsByTemplatePath(requests);
return processGroupedRequests(groupedRequests);
}

async function groupRequestsByTemplatePath(
templateToCodeDiff: Map<TemplateNode, CodeDiffRequest>,
requests: CodeDiffRequest[],
): Promise<Map<string, RequestsByPath>> {
const groupedRequests: Map<string, RequestsByPath> = new Map();

for (const [templateNode, request] of templateToCodeDiff) {
const codeBlock = await readFile(templateNode.path);
const path = templateNode.path;
for (const request of requests) {
const codeBlock = await readFile(request.templateNode.path);
const path = request.templateNode.path;

let groupedRequest = groupedRequests.get(path);
if (!groupedRequest) {
groupedRequest = { templateToCodeDiff: new Map(), codeBlock };
}
groupedRequest.templateToCodeDiff.set(templateNode, request);
groupedRequest.templateToCodeDiff.set(request.templateNode, request);
groupedRequests.set(path, groupedRequest);
}

Expand All @@ -44,7 +42,7 @@ function processGroupedRequests(groupedRequests: Map<string, RequestsByPath>): C

for (const [path, request] of groupedRequests) {
const { templateToCodeDiff, codeBlock } = request;
const ast = parseJsx(codeBlock);
const ast = parseJsxFile(codeBlock);
if (!ast) {
continue;
}
Expand Down
91 changes: 76 additions & 15 deletions app/electron/main/code/diff/transform.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import traverse, { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { twMerge } from 'tailwind-merge';
import { parseJsxCodeBlock } from '../helpers';
import { getTemplateNode } from '../templateNode';
import { EditorAttributes } from '/common/constants';
import { assertNever } from '/common/helpers';
import { InsertPos } from '/common/models';
import { CodeDiffRequest } from '/common/models/code';
import {
DomActionElement,
DomActionType,
CodeActionElement,
CodeActionType,
InsertedElement,
MovedElementWithTemplate,
} from '/common/models/element/domAction';
} from '/common/models/element/codeAction';
import { TemplateNode } from '/common/models/element/templateNode';

export function transformAst(
Expand All @@ -36,7 +38,11 @@ export function transformAst(
if (codeDiffRequest.textContent !== undefined) {
updateNodeTextContent(path.node, codeDiffRequest.textContent);
}
const structureChangeElements = getStructureChangeElements(codeDiffRequest);
const structureChangeElements = [
...codeDiffRequest.insertedElements,
...codeDiffRequest.movedElements,
...codeDiffRequest.removedElements,
];
applyStructureChanges(path, filepath, structureChangeElements);
}
},
Expand All @@ -58,26 +64,30 @@ function hashTemplateNode(node: TemplateNode): string {
return `${node.path}:${node.startTag.start.line}:${node.startTag.start.column}:${node.startTag.end.line}:${node.startTag.end.column}`;
}

function getStructureChangeElements(request: CodeDiffRequest): DomActionElement[] {
return [...request.insertedElements, ...request.movedElements];
}

function applyStructureChanges(
path: NodePath<t.JSXElement>,
filepath: string,
elements: DomActionElement[],
elements: CodeActionElement[],
): void {
for (const element of elements) {
if (element.type === DomActionType.MOVE) {
moveElementInNode(path, filepath, element as MovedElementWithTemplate);
} else if (element.type === DomActionType.INSERT) {
insertElementToNode(path, element as InsertedElement);
switch (element.type) {
case CodeActionType.MOVE:
moveElementInNode(path, filepath, element as MovedElementWithTemplate);
break;
case CodeActionType.INSERT:
insertElementToNode(path, element);
break;
case CodeActionType.REMOVE:
removeElementFromNode(path, element);
break;
default:
assertNever(element);
}
}
}

function insertElementToNode(path: NodePath<t.JSXElement>, element: InsertedElement): void {
const newElement = createJSXElement(element);
const newElement = createInsertedElement(element);

switch (element.location.position) {
case InsertPos.APPEND:
Expand All @@ -88,7 +98,7 @@ function insertElementToNode(path: NodePath<t.JSXElement>, element: InsertedElem
break;
case InsertPos.INDEX:
// Note: children includes non-JSXElement which our index does not account for. We need to find the JSXElement/JSXFragment-only index.
if (element.location.index !== undefined) {
if (element.location.index !== -1) {
const jsxElements = path.node.children.filter(
(child) => t.isJSXElement(child) || t.isJSXFragment(child),
) as t.JSXElement[];
Expand Down Expand Up @@ -116,6 +126,57 @@ function insertElementToNode(path: NodePath<t.JSXElement>, element: InsertedElem
path.stop();
}

function removeElementFromNode(path: NodePath<t.JSXElement>, element: CodeActionElement): void {
const children = path.node.children;
const jsxElements = children.filter(
(child) => t.isJSXElement(child) || t.isJSXFragment(child),
) as Array<t.JSXElement | t.JSXFragment>;
let elementToRemoveIndex: number;

switch (element.location.position) {
case InsertPos.INDEX:
if (element.location.index !== -1) {
elementToRemoveIndex = Math.min(element.location.index, jsxElements.length - 1);
} else {
console.error('Invalid index: undefined');
return;
}
break;
case InsertPos.APPEND:
elementToRemoveIndex = jsxElements.length - 1;
break;
case InsertPos.PREPEND:
elementToRemoveIndex = 0;
break;
default:
console.error(`Unhandled position: ${element.location.position}`);
return;
}

if (elementToRemoveIndex >= 0 && elementToRemoveIndex < jsxElements.length) {
const elementToRemove = jsxElements[elementToRemoveIndex];
const indexInChildren = children.indexOf(elementToRemove);

if (indexInChildren !== -1) {
children.splice(indexInChildren, 1);
} else {
console.error('Element to be removed not found in children');
}
} else {
console.error('Invalid element index for removal');
}

path.stop();
}

function createInsertedElement(insertedChild: InsertedElement): t.JSXElement {
if (insertedChild.codeBlock) {
const ast = parseJsxCodeBlock(insertedChild.codeBlock);
return ast as t.JSXElement;
}
return createJSXElement(insertedChild);
}

function createJSXElement(insertedChild: InsertedElement): t.JSXElement {
const attributes = Object.entries(insertedChild.attributes || {}).map(([key, value]) =>
t.jsxAttribute(
Expand Down
22 changes: 21 additions & 1 deletion app/electron/main/code/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function removeSemiColonIfApplicable(code: string, original: string) {
return code;
}

export function parseJsx(code: string): t.File | undefined {
export function parseJsxFile(code: string): t.File | undefined {
try {
return parse(code, {
plugins: ['typescript', 'jsx'],
Expand All @@ -20,3 +20,23 @@ export function parseJsx(code: string): t.File | undefined {
return;
}
}

export function parseJsxCodeBlock(code: string): t.JSXElement | undefined {
const ast = parseJsxFile(code);
if (!ast) {
return undefined;
}

const jsxElement = ast.program.body.find(
(node) => t.isExpressionStatement(node) && t.isJSXElement(node.expression),
);

if (
jsxElement &&
t.isExpressionStatement(jsxElement) &&
t.isJSXElement(jsxElement.expression)
) {
return jsxElement.expression;
}
return undefined;
}
4 changes: 2 additions & 2 deletions app/electron/main/code/moveKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import traverse, { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { generateCode } from './diff';
import { formatContent, readFile, writeFile } from './files';
import { parseJsx } from './helpers';
import { parseJsxFile } from './helpers';
import { EditorAttributes } from '/common/constants';

export async function cleanMoveKeys(files: string[]) {
try {
for (const file of files) {
const fileContent = await readFile(file);
const ast = parseJsx(fileContent);
const ast = parseJsxFile(fileContent);
if (!ast) {
continue;
}
Expand Down
Loading

0 comments on commit 5a9cd7d

Please sign in to comment.