Skip to content

Commit

Permalink
Built-in code editor
Browse files Browse the repository at this point in the history
Merge pull request #90 from haroldo-ok/editor

Implemented a code editor. It can be invoked by calling:

```bash
node . edit
```

It will open a backend on port 1235, a frontend in port 1234 and will launch a browser instance.

This fixes #88
  • Loading branch information
haroldo-ok committed Dec 23, 2022
2 parents 0dad497 + 875650a commit a3e17ae
Show file tree
Hide file tree
Showing 24 changed files with 15,232 additions and 3,657 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,6 @@ dist
/examples/test/src/*
/examples/test/res
/examples/test/src

/.cache
/.parcel-cache
8 changes: 8 additions & 0 deletions .proxyrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"/api": {
"target": "http://localhost:1235/",
"pathRewrite": {
"^/api": ""
}
}
}
2 changes: 1 addition & 1 deletion base/src/vn_engine.c
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ void VN_init() {
XGM_setForceDelayDMA(TRUE);

VDP_setTextPalette(TEXT_PAL);
VDP_drawText("choice4genesis v0.12.0", 17, 27);
VDP_drawText("choice4genesis v0.13.0", 17, 27);
}


Expand Down
79 changes: 79 additions & 0 deletions editor/back/backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

const express = require('express');
const { fork } = require('child_process');

const { listProjectNames, listProjectScenes, openProjectOnExplorer, readProjectScene, writeProjectScene } = require('../../generator/project');

const startBackend = (commandLine, port) => {
const errorHandler = (err, req, res, next) => {
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;;
console.error(`Error ${err} at URL ${fullUrl}`, err);
res.status(500).send({ url: fullUrl, error: err });
}

const api = express.Router();
api.get('/projects', async (req, res) => {
try {
res.send(await listProjectNames(commandLine))
} catch (e) {
errorHandler(e, req, res);
}
});

api.get('/projects/:project/scenes', async (req, res) => {
try {
res.send(await listProjectScenes(commandLine, req.params.project))
} catch (e) {
errorHandler(e, req, res);
}
});

api.get('/projects/:project/scenes/:scene', async (req, res) => {
try {
res.type('txt');
res.send(await readProjectScene(commandLine, req.params.project, req.params.scene));
} catch (e) {
errorHandler(e, req, res);
}
});

api.put('/projects/:project/scenes/:scene', express.text(), async (req, res) => {
try {
await writeProjectScene(commandLine, req.params.project, req.params.scene, req.body)
res.send({ message: 'File saved.' });
} catch (e) {
errorHandler(e, req, res);
}
});

api.post('/projects/:project/run', async (req, res) => {
try {
const child = fork('.', ['transpile', req.params.project, '--', 'compile', 'emulate']);
child.on('exit', () => {
console.log('Execution OK');
res.send({ message: 'Execution OK' });
});
} catch (e) {
errorHandler(e, req, res);
}
});

api.post('/projects/:project/explore', async (req, res) => {
try {
openProjectOnExplorer(commandLine, req.params.project);
res.send({ message: 'Execution OK' });
} catch (e) {
errorHandler(e, req, res);
}
});

const app = express();

app.use('/v0', api);
app.listen(port);

console.log(`Backend running on port ${port}`);
};

module.exports = { startBackend };
35 changes: 35 additions & 0 deletions editor/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const PARCEL_PORT = 1234;
const API_PORT = 1235;

const showEditor = async (commandLine, executeCommands) => {
const { Parcel } = require('@parcel/core');
const { openInBrowser } = require('@parcel/utils');
const { normalize } = require('path');

const { startBackend } = require('./back/backend');

startBackend(commandLine, API_PORT);

let bundler = new Parcel({
entries: normalize(__dirname + '/front/index.html'),
defaultConfig: '@parcel/config-default',
shouldAutoInstall: true,
serveOptions: {
port: PARCEL_PORT
},
hmrOptions: {
port: PARCEL_PORT
}
});

await bundler.watch();
console.log(`Frontend running on port ${PARCEL_PORT}`);

if (commandLine.openBrowser) {
openInBrowser(`http://localhost:${PARCEL_PORT}/`);
}
};

module.exports = { showEditor };
10 changes: 10 additions & 0 deletions editor/front/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.editContainer {
}

.editContainer > nav {
float: left;
}

.editContainer > div {
overflow: auto;
}
100 changes: 100 additions & 0 deletions editor/front/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react';
import Editor, { DiffEditor, useMonaco, loader } from "@monaco-editor/react";

import '@picocss/pico/css/pico.min.css'
import 'font-awesome/css/font-awesome.min.css'
import './App.css'

import { prepareSyntax } from './syntax'
import { ProjectList } from './ProjectList'
import { SceneList } from './SceneList'
import { Scene } from './Scene'
import { RunButton } from './RunButton'
import { SaveAllButton } from './SaveAllButton'
import { OpenFileExplorer } from './OpenFileExplorer'

import { callSaveSceneApi } from './hooks/api'

export function App() {
prepareSyntax();

const [projectName, setProjectName] = useState("");
const [sceneName, setSceneName] = useState("");
const [scenes, setScenes] = useState({});

// TODO: The code that keeps track of whether the scene's data has been changed is a big mess, right now.

const handleSceneDataChange = (sceneInfo) => {
const { originalData, data } = sceneInfo;
const lastData = sceneInfo.lastData === undefined ? (scenes[sceneName] || {}).lastData : sceneInfo.lastData;
const comparedData = lastData === undefined ? originalData : lastData;
const isModified = comparedData != data;

const newScenes = { ...scenes };
newScenes[sceneName] = { ...sceneInfo, lastData, isModified };
setScenes(newScenes);
}

const isModifiedScene = sceneName => (scenes[sceneName] || {}).isModified;

const saveScene = async sceneName => {
const sceneInfo = scenes[sceneName];
const { data } = sceneInfo;
await callSaveSceneApi(projectName, sceneName, data);
handleSceneDataChange({ ...sceneInfo, lastData: data });
};

const saveAll = async () => {
const promises = Object.entries(scenes)
.filter(([sceneName, { isModified }]) => isModified)
.map(([sceneName]) => saveScene(sceneName));

await Promise.all(promises);

setScenes(Object.fromEntries(Object.entries(scenes).map(([sceneName, sceneInfo]) => {
const { data } = sceneInfo;
return [sceneName, { ...sceneInfo, lastData: data, isModified: false }]
})));
};

if (!projectName) {
return (
<main className="container">
<nav>
<article>
<header>Select a project to edit...</header>
<ProjectList value={projectName} onChange={setProjectName} />
</article>
</nav>
</main>
);
}

return (
<div>
<nav>
<ul>
<li><SaveAllButton projectName={projectName} onClick={saveAll} /></li>
<li><OpenFileExplorer projectName={projectName} /></li>
</ul>
<ul>
<li><strong>choice4genesis editor</strong></li>
</ul>
<ul>
<li><RunButton projectName={projectName} onSaveAll={saveAll} /></li>
</ul>
</nav>
<div className="editContainer">
<nav>
<article>
<header>Scenes</header>
<SceneList projectName={projectName} value={sceneName} onChange={setSceneName} onSaveScene={saveScene} isModified={isModifiedScene} />
</article>
</nav>
<div>
<Scene projectName={projectName} sceneName={sceneName} onChange={handleSceneDataChange} />
</div>
</div>
</div>
);
}
22 changes: 22 additions & 0 deletions editor/front/OpenFileExplorer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolderOpen } from '@fortawesome/free-solid-svg-icons'

import { callOpenExplorerApi } from './hooks/api';

export function OpenFileExplorer(props) {
const handleButtonClick = async e => {
try {
await callOpenExplorerApi(props.projectName);
} catch (e) {
console.error('Error while opening explorer', e);
}
};

return (
<a href="#" role="button" onClick={handleButtonClick} className="secondary">
<FontAwesomeIcon icon={faFolderOpen} /> View Files
</a>
);
}
33 changes: 33 additions & 0 deletions editor/front/ProjectList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useProjectListApi } from './hooks/api';

export function ProjectList(props) {
const { data, error } = useProjectListApi();

if (error) {
console.error('Error while listing projects', error);
return <h1>Error while listing projects</h1>;
}
if (!data) return <h1>Loading project list...</h1>;

const selectedValue = props.value && data.find(projectName => projectName === props.value) ? props.value : '';

const handleProjectNameChange = projectName => {
props.onChange && props.onChange(projectName);
};

return (
<aside>
<nav>
<ul>
{data.map(projectName =>
<li key={projectName}>
<a href="#" className={projectName === selectedValue ? '' : 'secondary'} onClick={() => handleProjectNameChange(projectName)}>
{projectName}
</a>
</li>
)}
</ul>
</nav>
</aside>
);
}
32 changes: 32 additions & 0 deletions editor/front/RunButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

import React, { useState } from 'react';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNotch, faPlay } from '@fortawesome/free-solid-svg-icons'

import { callRunApi } from './hooks/api';

export function RunButton(props) {
if (!props.projectName) return <a href="#" role="button" disabled={true}><FontAwesomeIcon icon={faPlay} /> Run</a>;

const [processing, setProcessing] = useState(false);

const handleButtonClick = async e => {
setProcessing(true);
try {
props.onSaveAll && await props.onSaveAll();
await callRunApi(props.projectName);
} catch (e) {
console.error('Error while executing', e);
} finally {
setProcessing(false);
}
};

return (
<a href="#" role="button" disabled={processing} onClick={handleButtonClick}>
<FontAwesomeIcon icon={processing ? faCircleNotch : faPlay} className={processing ? 'fa-spin' : ''} /> Run
</a>
);
}
27 changes: 27 additions & 0 deletions editor/front/SaveAllButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

import React, { useState } from 'react';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNotch, faFloppyDisk } from '@fortawesome/free-solid-svg-icons'

export function SaveAllButton(props) {
const [processing, setProcessing] = useState(false);

const handleButtonClick = async e => {
setProcessing(true);
try {
await props.onClick(e);
} catch (e) {
console.error('Error while executing', e);
} finally {
setProcessing(false);
}
};

return (
<a href="#" role="button" disabled={processing} onClick={handleButtonClick} className="secondary">
<FontAwesomeIcon icon={processing ? faCircleNotch : faFloppyDisk} className={processing ? 'fa-spin' : ''} /> Save all
</a>
);
}
Loading

0 comments on commit a3e17ae

Please sign in to comment.