// SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors import './editor.scss'; import { Button, Classes, IOverlayLifecycleProps, IconName, Menu, MenuDivider, MenuItem, Tab, TabId, Tabs, Text, } from '@blueprintjs/core'; import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; import * as monaco from 'monaco-editor'; import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json'; import xcodeTheme from 'monaco-themes/themes/Xcode_default.json'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useId } from 'react-aria'; import { useDispatch } from 'react-redux'; import { useEffectOnce, useTernaryDarkMode } from 'usehooks-ts'; import { UUID } from '../fileStorage'; import { useFileStoragePath } from '../fileStorage/hooks'; import { compile } from '../mpy/actions'; import { useSelector } from '../reducers'; import { useSettingIsShowDocsEnabled } from '../settings/hooks'; import { isMacOS } from '../utils/os'; import Welcome from './Welcome'; import { editorActivateFile, editorCloseFile } from './actions'; import { useI18n } from './i18n'; import * as pybricksMicroPython from './pybricksMicroPython'; import { pybricksMicroPythonId } from './pybricksMicroPython'; import { UntitledHintContribution } from './untitledHint'; monaco.languages.register({ id: pybricksMicroPythonId }); const toDispose = new Array(); toDispose.push( monaco.languages.setLanguageConfiguration( pybricksMicroPythonId, pybricksMicroPython.conf, ), monaco.languages.setMonarchTokensProvider( pybricksMicroPythonId, pybricksMicroPython.language, ), monaco.languages.registerCompletionItemProvider( pybricksMicroPythonId, pybricksMicroPython.templateSnippetCompletions, ), ); // https://webpack.js.org/api/hot-module-replacement/ // istanbul ignore if: only used for development if (module.hot) { module.hot.dispose(() => { toDispose.forEach((s) => s.dispose()); }); } const tomorrowNightEightiesId = 'tomorrow-night-eighties'; monaco.editor.defineTheme( tomorrowNightEightiesId, tomorrowNightEightiesTheme as monaco.editor.IStandaloneThemeData, ); const xcodeId = 'xcode'; monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData); type EditorContextMenuItemProps = Readonly<{ /** The menu item label. */ label: string; /** The menu item icon. */ icon: IconName; /** The keyboard shortcut that triggers the same action. */ keyboardShortcut: string; /** Controls the menu item disabled state. */ disabled: boolean; /** A reference to the editor. */ editor: monaco.editor.IStandaloneCodeEditor | undefined; /** The action handler ID passed to the editor.trigger() method. */ editorAction: string; }>; const EditorContextMenuItem: React.VoidFunctionComponent< EditorContextMenuItemProps > = ({ label, icon, keyboardShortcut, disabled, editor, editorAction }) => { const labelId = useId(); return ( {label}} icon={icon} label={keyboardShortcut} disabled={disabled} onClick={() => { // have to focus first or the trigger won't work editor?.focus(); editor?.trigger(null, editorAction, null); }} /> ); }; type EditorContextMenuProps = { /** The editor. */ editor: monaco.editor.IStandaloneCodeEditor | undefined; }; const EditorContextMenu: React.VoidFunctionComponent = ({ editor, }) => { const i18n = useI18n(); const selection = editor?.getSelection(); const hasSelection = selection && !selection.isEmpty(); const model = editor?.getModel(); const canUndo = model && model.canUndo(); const canRedo = model && model.canRedo(); return ( ); }; type FileNameProps = { /** The DOM ID. */ id: string; /** The file UUID. */ uuid: UUID; /** Called when the file name changes. */ onNameChanged: () => void; }; const TabLabel: React.VoidFunctionComponent = ({ id, uuid, onNameChanged, }) => { const fileName = useFileStoragePath(uuid); useEffect(() => { onNameChanged?.(); }, [fileName, onNameChanged]); return ( {fileName} ); }; type TabCloseButtonProps = { /** The file UUID. */ uuid: UUID; }; const TabCloseButton: React.VoidFunctionComponent = ({ uuid }) => { const fileName = useFileStoragePath(uuid) ?? ''; const dispatch = useDispatch(); const i18n = useI18n(); return (