Merge pull request #3422 from danielyxie/revert-2876-feature/monaco-theme-editor

Revert "[Feature] Script Editor Theme Customization"
This commit is contained in:
hydroflame 2022-04-13 01:13:03 -04:00 committed by GitHub
commit 476e9df5bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 5 additions and 526 deletions

@ -9,10 +9,6 @@ import Select from "@mui/material/Select";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import { ThemeEditorModal } from "./ThemeEditorModal";
interface IProps { interface IProps {
options: Options; options: Options;
@ -27,7 +23,6 @@ export function OptionsModal(props: IProps): React.ReactElement {
const [fontSize, setFontSize] = useState(props.options.fontSize); const [fontSize, setFontSize] = useState(props.options.fontSize);
const [wordWrap, setWordWrap] = useState(props.options.wordWrap); const [wordWrap, setWordWrap] = useState(props.options.wordWrap);
const [vim, setVim] = useState(props.options.vim); const [vim, setVim] = useState(props.options.vim);
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
function save(): void { function save(): void {
props.save({ props.save({
@ -48,10 +43,6 @@ export function OptionsModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<ThemeEditorModal
open={themeEditorOpen}
onClose={() => setThemeEditorOpen(false)}
/>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<Typography>Theme: </Typography> <Typography>Theme: </Typography>
<Select onChange={(event) => setTheme(event.target.value)} value={theme}> <Select onChange={(event) => setTheme(event.target.value)} value={theme}>
@ -62,11 +53,7 @@ export function OptionsModal(props: IProps): React.ReactElement {
<MenuItem value="light">light</MenuItem> <MenuItem value="light">light</MenuItem>
<MenuItem value="dracula">dracula</MenuItem> <MenuItem value="dracula">dracula</MenuItem>
<MenuItem value="one-dark">one-dark</MenuItem> <MenuItem value="one-dark">one-dark</MenuItem>
<MenuItem value="customTheme">Custom theme</MenuItem>
</Select> </Select>
<Button onClick={() => setThemeEditorOpen(true)} sx={{ mx: 1 }} startIcon={<EditIcon />}>
Edit custom theme
</Button>
</Box> </Box>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
@ -93,7 +80,7 @@ export function OptionsModal(props: IProps): React.ReactElement {
<TextField type="number" label="Font size" value={fontSize} onChange={onFontChange} /> <TextField type="number" label="Font size" value={fontSize} onChange={onFontChange} />
</Box> </Box>
<br /> <br />
<Button onClick={save} startIcon={<SaveIcon />}>Save</Button> <Button onClick={save}>Save</Button>
</Modal> </Modal>
); );
} }

@ -25,7 +25,7 @@ import { Settings } from "../../Settings/Settings";
import { iTutorialNextStep, ITutorial, iTutorialSteps } from "../../InteractiveTutorial"; import { iTutorialNextStep, ITutorial, iTutorialSteps } from "../../InteractiveTutorial";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { saveObject } from "../../SaveObject"; import { saveObject } from "../../SaveObject";
import { loadThemes, makeTheme, sanitizeTheme } from "./themes"; import { loadThemes } from "./themes";
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -362,8 +362,6 @@ export function Root(props: IProps): React.ReactElement {
monaco.languages.typescript.javascriptDefaults.addExtraLib(source, "netscript.d.ts"); monaco.languages.typescript.javascriptDefaults.addExtraLib(source, "netscript.d.ts");
monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts"); monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
loadThemes(monaco); loadThemes(monaco);
sanitizeTheme(Settings.EditorTheme);
monaco.editor.defineTheme("customTheme", makeTheme(Settings.EditorTheme));
} }
// When the editor is mounted // When the editor is mounted
@ -995,11 +993,7 @@ export function Root(props: IProps): React.ReactElement {
</Box> </Box>
<OptionsModal <OptionsModal
open={optionsOpen} open={optionsOpen}
onClose={() => { onClose={() => setOptionsOpen(false)}
sanitizeTheme(Settings.EditorTheme);
monacoRef.current?.editor.defineTheme("customTheme", makeTheme(Settings.EditorTheme));
setOptionsOpen(false);
}}
options={{ options={{
theme: Settings.MonacoTheme, theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces, insertSpaces: Settings.MonacoInsertSpaces,
@ -1008,8 +1002,6 @@ export function Root(props: IProps): React.ReactElement {
vim: Settings.MonacoVim, vim: Settings.MonacoVim,
}} }}
save={(options: Options) => { save={(options: Options) => {
sanitizeTheme(Settings.EditorTheme);
monacoRef.current?.editor.defineTheme("customTheme", makeTheme(Settings.EditorTheme));
setOptions(options); setOptions(options);
Settings.MonacoTheme = options.theme; Settings.MonacoTheme = options.theme;
Settings.MonacoInsertSpaces = options.insertSpaces; Settings.MonacoInsertSpaces = options.insertSpaces;

@ -1,276 +0,0 @@
import React, { useState } from "react";
import { Modal } from "../../ui/React/Modal";
import { defaultMonacoTheme, IScriptEditorTheme } from "./themes";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Tooltip from "@mui/material/Tooltip";
import TextField from "@mui/material/TextField";
import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton";
import ReplyIcon from "@mui/icons-material/Reply";
import HistoryIcon from '@mui/icons-material/History';
import SaveIcon from '@mui/icons-material/Save';
import { Settings } from "../../Settings/Settings";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import _ from "lodash";
import { Color, ColorPicker } from "material-ui-color";
interface IProps {
onClose: () => void;
open: boolean;
}
interface IColorEditorProps {
label: string;
themePath: string;
color: string | undefined;
onColorChange: (name: string, value: string) => void;
defaultColor: string;
}
// Slightly tweaked version of the same function found in game options
function ColorEditor({ label, themePath, onColorChange, color, defaultColor }: IColorEditorProps): React.ReactElement {
if (color === undefined) {
console.error(`color ${themePath} was undefined, reverting to default`);
color = defaultColor;
}
return (
<>
<Tooltip title={label}>
<span>
<TextField
label={themePath}
value={"#" + color}
sx={{ display: "block", my: 1 }}
InputProps={{
startAdornment: (
<>
<ColorPicker
hideTextfield
deferred
value={"#" + color}
onChange={(newColor: Color) => onColorChange(themePath, newColor.hex)}
disableAlpha
/>
</>
),
endAdornment: (
<>
<IconButton onClick={() => onColorChange(themePath, defaultColor)}>
<ReplyIcon color="primary" />
</IconButton>
</>
),
}}
/>
</span>
</Tooltip>
</>
);
}
export function ThemeEditorModal(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((o) => !o);
}
// Need to deep copy the object since it has nested attributes
const [themeCopy, setThemeCopy] = useState<IScriptEditorTheme>(JSON.parse(JSON.stringify(Settings.EditorTheme)));
function onColorChange(name: string, value: string): void {
setThemeCopy(_.set(themeCopy, name, value));
rerender();
}
function onThemeChange(event: React.ChangeEvent<HTMLInputElement>): void {
try {
const importedTheme = JSON.parse(event.target.value);
if (typeof importedTheme !== "object") return;
setThemeCopy(importedTheme);
} catch (err) {
// ignore
}
}
return (
<Modal open={props.open} onClose={() => {
setThemeCopy(Settings.EditorTheme);
props.onClose();
}}>
<Typography variant="h4">Customize Editor theme</Typography>
<Typography>Hover over input boxes for more information</Typography>
<Paper sx={{ p: 1, my: 1 }}>
<OptionSwitch
checked={themeCopy.base === "vs"}
onChange={(val) => {
setThemeCopy(_.set(themeCopy, "base", val ? "vs" : "vs-dark"));
rerender();
}}
text="Use light theme as base"
tooltip={
<>
If enabled, the <code>vs</code> light theme will be used as the
theme base, otherwise, <code>vs-dark</code> will be used.
</>
}
/>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "fit-content", gap: 1 }}>
<Box>
<Typography variant="h6">UI</Typography>
<ColorEditor
label="Background color"
themePath="common.bg"
onColorChange={onColorChange}
color={themeCopy.common.bg}
defaultColor={defaultMonacoTheme.common.bg}
/>
<ColorEditor
label="Current line and minimap background color"
themePath="ui.line"
onColorChange={onColorChange}
color={themeCopy.ui.line}
defaultColor={defaultMonacoTheme.ui.line}
/>
<ColorEditor
label="Base text color"
themePath="common.fg"
onColorChange={onColorChange}
color={themeCopy.common.fg}
defaultColor={defaultMonacoTheme.common.fg}
/>
<ColorEditor
label="Popup background color"
themePath="ui.panel.bg"
onColorChange={onColorChange}
color={themeCopy.ui.panel.bg}
defaultColor={defaultMonacoTheme.ui.panel.bg}
/>
<ColorEditor
label="Background color for selected item in popup"
themePath="ui.panel.selected"
onColorChange={onColorChange}
color={themeCopy.ui.panel.selected}
defaultColor={defaultMonacoTheme.ui.panel.selected}
/>
<ColorEditor
label="Popup border color"
themePath="ui.panel.border"
onColorChange={onColorChange}
color={themeCopy.ui.panel.border}
defaultColor={defaultMonacoTheme.ui.panel.border}
/>
<ColorEditor
label="Background color of highlighted text"
themePath="ui.selection.bg"
onColorChange={onColorChange}
color={themeCopy.ui.selection.bg}
defaultColor={defaultMonacoTheme.ui.selection.bg}
/>
</Box>
<Box>
<Typography variant="h6">Syntax</Typography>
<ColorEditor
label="Numbers, function names, and other key vars"
themePath="common.accent"
onColorChange={onColorChange}
color={themeCopy.common.accent}
defaultColor={defaultMonacoTheme.common.accent}
/>
<ColorEditor
label="Keywords"
themePath="syntax.keyword"
onColorChange={onColorChange}
color={themeCopy.syntax.keyword}
defaultColor={defaultMonacoTheme.syntax.keyword}
/>
<ColorEditor
label="Strings"
themePath="syntax.string"
onColorChange={onColorChange}
color={themeCopy.syntax.string}
defaultColor={defaultMonacoTheme.syntax.string}
/>
<ColorEditor
label="Regexp literals as well as escapes within strings"
themePath="syntax.regexp"
onColorChange={onColorChange}
color={themeCopy.syntax.regexp}
defaultColor={defaultMonacoTheme.syntax.regexp}
/>
<ColorEditor
label="Constants"
themePath="syntax.constant"
onColorChange={onColorChange}
color={themeCopy.syntax.constant}
defaultColor={defaultMonacoTheme.syntax.constant}
/>
<ColorEditor
label="Entities"
themePath="syntax.entity"
onColorChange={onColorChange}
color={themeCopy.syntax.entity}
defaultColor={defaultMonacoTheme.syntax.entity}
/>
<ColorEditor
label="'this', 'ns', types, and tags"
themePath="syntax.tag"
onColorChange={onColorChange}
color={themeCopy.syntax.tag}
defaultColor={defaultMonacoTheme.syntax.tag}
/>
<ColorEditor
label="Netscript functions and constructors"
themePath="syntax.markup"
onColorChange={onColorChange}
color={themeCopy.syntax.markup}
defaultColor={defaultMonacoTheme.syntax.markup}
/>
<ColorEditor
label="Errors"
themePath="syntax.error"
onColorChange={onColorChange}
color={themeCopy.syntax.error}
defaultColor={defaultMonacoTheme.syntax.error}
/>
<ColorEditor
label="Comments"
themePath="syntax.comment"
onColorChange={onColorChange}
color={themeCopy.syntax.comment}
defaultColor={defaultMonacoTheme.syntax.comment}
/>
</Box>
</Box>
</Paper>
<Paper sx={{ p: 1 }}>
<TextField
multiline
fullWidth
maxRows={10}
label={"import / export theme"}
value={JSON.stringify(themeCopy, undefined, 2)}
onChange={onThemeChange}
/>
<Box sx={{ mt: 1 }}>
<Button onClick={() => {
Settings.EditorTheme = { ...themeCopy };
props.onClose()
}} startIcon={<SaveIcon />}>Save</Button>
<Button
onClick={() => {
setThemeCopy(defaultMonacoTheme);
rerender();
}}
startIcon={<HistoryIcon />}
>Reset to default</Button>
</Box>
</Paper>
</Modal>
)
}

@ -1,216 +1,3 @@
export interface IScriptEditorTheme {
[key: string]: any;
base: string;
inherit: boolean;
common: {
[key: string]: string;
accent: string;
bg: string;
fg: string;
};
syntax: {
[key: string]: string;
tag: string;
entity: string;
string: string;
regexp: string;
markup: string;
keyword: string;
comment: string;
constant: string;
error: string;
};
ui: {
[key: string]: any;
line: string;
panel: {
[key: string]: string;
bg: string;
selected: string;
border: string;
};
selection: {
[key: string]: string;
bg: string;
};
};
}
export const defaultMonacoTheme: IScriptEditorTheme = {
base: "vs-dark",
inherit: true,
common: {
accent: "B5CEA8",
bg: "1E1E1E",
fg: "D4D4D4",
},
syntax: {
tag: "569CD6",
entity: "569CD6",
string: "CE9178",
regexp: "646695",
markup: "569CD6",
keyword: "569CD6",
comment: "6A9955",
constant: "569CD6",
error: "F44747"
},
ui: {
line: "1E1E1E",
panel: {
bg: "252526",
selected: "252526",
border: "1E1E1E"
},
selection: {
bg: "ADD6FF26"
}
}
}
// Regex used for token color validation
// https://github.com/microsoft/vscode/blob/973684056e67153952f495fce93bf50d0ec0b892/src/vs/editor/common/languages/supports/tokenization.ts#L153
const colorRegExp = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/;
// Recursively sanitize the theme data to prevent errors
// Invalid data will be replaced with FF0000 (bright red)
export const sanitizeTheme = (theme: IScriptEditorTheme): void => {
for (const [k, v] of Object.entries(theme)) {
switch (k) {
case "base":
if (!["vs-dark", "vs"].includes(theme.base)) theme.base = "vs-dark";
continue;
case "inherit":
if (typeof theme.inherit !== "boolean") theme.inherit = true;
continue;
}
const repairBlock = (block: { [key: string]: any }): void => {
for (const [k, v] of Object.entries(block)) {
if (typeof v === "object") {
repairBlock(v as { [key: string]: string });
} else {
if (!v.match(colorRegExp)) block[k] = "FF0000";
}
}
}
repairBlock(v);
}
}
export function makeTheme(theme: IScriptEditorTheme): any {
const themeRules = [
{
token: "",
background: theme.ui.line,
foreground: theme.common.fg
},
{
token: "identifier",
foreground: theme.common.accent
},
{
token: "keyword",
foreground: theme.syntax.keyword
},
{
token: "string",
foreground: theme.syntax.string
},
{
token: "string.escape",
foreground: theme.syntax.regexp
},
{
token: "comment",
foreground: theme.syntax.comment
},
{
token: "constant",
foreground: theme.syntax.constant
},
{
token: "entity",
foreground: theme.syntax.entity
},
{
token: "type",
foreground: theme.syntax.tag
},
{
token: "tag",
foreground: theme.syntax.tag
},
{
token: "regexp",
foreground: theme.syntax.regexp
},
{
token: "attribute",
foreground: theme.syntax.tag
},
{
token: "constructor",
foreground: theme.syntax.markup
},
{
token: "invalid",
foreground: theme.syntax.error
},
{
token: "number",
foreground: theme.common.accent
},
{
token: "delimiter",
foreground: theme.common.fg
},
// Custom tokens
{
token: "ns",
foreground: theme.syntax.tag
},
{
token: "netscriptfunction",
foreground: theme.syntax.markup
},
{
token: "otherkeywords",
foreground: theme.syntax.keyword
},
{
token: "otherkeyvars",
foreground: theme.common.accent
},
{
token: "this",
foreground: theme.syntax.tag
}
];
const themeColors = Object.fromEntries([
["editor.background", theme.common.bg],
["editor.foreground", theme.common.fg],
["editor.lineHighlightBackground", theme.ui.line],
["editor.selectionBackground", theme.ui.selection.bg],
["editorSuggestWidget.background", theme.ui.panel.bg],
["editorSuggestWidget.border", theme.ui.panel.border],
["editorSuggestWidget.selectedBackground", theme.ui.panel.selected],
["editorHoverWidget.background", theme.ui.panel.bg],
["editorHoverWidget.border", theme.ui.panel.border],
["editorWidget.background", theme.ui.panel.bg],
["editorWidget.border", theme.ui.panel.border],
["input.background", theme.ui.panel.bg],
["input.border", theme.ui.panel.border]
].map(([k, v]) => [k, "#" + v]));
return { base: theme.base, inherit: theme.inherit, rules: themeRules, colors: themeColors }
}
export async function loadThemes(monaco: { editor: any }): Promise<void> { export async function loadThemes(monaco: { editor: any }): Promise<void> {
monaco.editor.defineTheme("monokai", { monaco.editor.defineTheme("monokai", {
base: "vs-dark", base: "vs-dark",

@ -5,7 +5,6 @@ import { defaultStyles } from "../Themes/Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options"; import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview"; import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions"; import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
import { defaultMonacoTheme, IScriptEditorTheme } from "../ScriptEditor/ui/themes";
/** /**
* Represents the default settings the player could customize. * Represents the default settings the player could customize.
@ -158,11 +157,6 @@ interface IDefaultSettings {
* If the game's sidebar is opened * If the game's sidebar is opened
*/ */
IsSidebarOpened: boolean; IsSidebarOpened: boolean;
/**
* Script editor theme data
*/
EditorTheme: IScriptEditorTheme;
} }
/** /**
@ -222,8 +216,6 @@ export const defaultSettings: IDefaultSettings = {
theme: defaultTheme, theme: defaultTheme,
styles: defaultStyles, styles: defaultStyles,
overview: { x: 0, y: 0, opened: true }, overview: { x: 0, y: 0, opened: true },
EditorTheme: defaultMonacoTheme,
}; };
/** /**
@ -270,7 +262,6 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
theme: { ...defaultTheme }, theme: { ...defaultTheme },
styles: { ...defaultStyles }, styles: { ...defaultStyles },
overview: defaultSettings.overview, overview: defaultSettings.overview,
EditorTheme: { ...defaultMonacoTheme },
init() { init() {
Object.assign(Settings, defaultSettings); Object.assign(Settings, defaultSettings);
}, },
@ -282,8 +273,6 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
delete save.styles; delete save.styles;
Object.assign(Settings.overview, save.overview); Object.assign(Settings.overview, save.overview);
delete save.overview; delete save.overview;
Object.assign(Settings.EditorTheme, save.EditorTheme);
delete save.EditorTheme;
Object.assign(Settings, save); Object.assign(Settings, save);
}, },
}; };

@ -366,9 +366,9 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
sx={{ mb: 1 }} sx={{ mb: 1 }}
multiline multiline
fullWidth fullWidth
maxRows={10} maxRows={3}
label={"import / export theme"} label={"import / export theme"}
value={JSON.stringify(customTheme, undefined, 2)} value={JSON.stringify(customTheme)}
onChange={onThemeChange} onChange={onThemeChange}
/> />
<> <>