Commit f03c435c authored by tom's avatar tom

hotkeys for code editor

parent 9dfd4a93
......@@ -19,3 +19,6 @@ export const minus = String.fromCharCode(8722); // −
export const leftLineArrow = String.fromCharCode(8592); // ←
export const rightLineArrow = String.fromCharCode(8594); // →
export const apos = String.fromCharCode(39); // apostrophe '
export const shift = String.fromCharCode(8679); // upwards white arrow ⇧
export const cmd = String.fromCharCode(8984); // place of interest sign ⌘
export const alt = String.fromCharCode(9095); // alternate key symbol ⎇
......@@ -39,11 +39,11 @@ interface Props {
const CodeEditor = ({ data }: Props) => {
const [ instance, setInstance ] = React.useState<Monaco | undefined>();
const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>();
const [ index, setIndex ] = React.useState(0);
const [ tabs, setTabs ] = React.useState([ data[index].file_path ]);
const [ isMetaPressed, setIsMetaPressed ] = React.useState(false);
const editorRef = React.useRef<monaco.editor.IStandaloneCodeEditor>();
const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>();
const { colorMode } = useColorMode();
......@@ -58,7 +58,7 @@ const CodeEditor = ({ data }: Props) => {
const handleEditorDidMount = React.useCallback((editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
setInstance(monaco);
editorRef.current = editor;
setEditor(editor);
monaco.editor.defineTheme('blockscout-light', themes.light);
monaco.editor.defineTheme('blockscout-dark', themes.dark);
......@@ -71,6 +71,23 @@ const CodeEditor = ({ data }: Props) => {
.map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path)));
loadedModels.concat(newModels).forEach(addFileImportDecorations);
editor.addAction({
id: 'close-tab',
label: 'Close current tab',
keybindings: [
monaco.KeyMod.Alt | monaco.KeyCode.KeyW,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.7,
run: function(editor) {
const model = editor.getModel();
const path = model?.uri.path;
if (path) {
handleTabClose(path, true);
}
},
});
// componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
......@@ -80,11 +97,11 @@ const CodeEditor = ({ data }: Props) => {
setTabs((prev) => prev.some((item) => item === data[index].file_path) ? prev : ([ ...prev, data[index].file_path ]));
if (lineNumber !== undefined && !Object.is(lineNumber, NaN)) {
window.setTimeout(() => {
editorRef.current?.revealLineInCenter(lineNumber);
editor?.revealLineInCenter(lineNumber);
}, 0);
}
editorRef.current?.focus();
}, [ data ]);
editor?.focus();
}, [ data, editor ]);
const handleTabSelect = React.useCallback((path: string) => {
const index = data.findIndex((item) => item.file_path === path);
......@@ -93,14 +110,14 @@ const CodeEditor = ({ data }: Props) => {
}
}, [ data ]);
const handleTabClose = React.useCallback((path: string) => {
const handleTabClose = React.useCallback((path: string, _isActive?: boolean) => {
setTabs((prev) => {
if (prev.length > 1) {
const tabIndex = prev.findIndex((item) => item === path);
const isActive = data[index].file_path === path;
const isActive = _isActive !== undefined ? _isActive : data[index].file_path === path;
if (isActive) {
const nextActiveIndex = data.findIndex((item) => item.file_path === prev[Math.max(0, tabIndex - 1)]);
const nextActiveIndex = data.findIndex((item) => item.file_path === prev[(tabIndex === 0 ? 1 : tabIndex - 1)]);
setIndex(nextActiveIndex);
}
......@@ -200,7 +217,13 @@ const CodeEditor = ({ data }: Props) => {
loading={ <CodeEditorLoading/> }
/>
</Box>
<CodeEditorSideBar data={ data } onFileSelect={ handleSelectFile } monaco={ instance } selectedFile={ data[index].file_path }/>
<CodeEditorSideBar
data={ data }
onFileSelect={ handleSelectFile }
monaco={ instance }
editor={ editor }
selectedFile={ data[index].file_path }
/>
</Flex>
);
};
......
......@@ -18,9 +18,10 @@ interface Props {
isInputStuck: boolean;
isActive: boolean;
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>;
defaultValue: string;
}
const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer }: Props) => {
const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer, defaultValue }: Props) => {
const [ searchTerm, changeSearchTerm ] = React.useState('');
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
......@@ -33,6 +34,10 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
const debouncedSearchTerm = useDebounce(searchTerm, 300);
React.useEffect(() => {
changeSearchTerm(defaultValue);
}, [ defaultValue ]);
React.useEffect(() => {
if (!monaco) {
return;
......@@ -166,7 +171,8 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
borderWidth="1px"
borderColor={ themeColors['input.background'] }
py="2px"
px="4px"
pl="4px"
pr="75px"
transitionDuration="0"
_focus={{
borderColor: themeColors.focusBorder,
......
import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
import _throttle from 'lodash/throttle';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react';
import type { File, Monaco } from './types';
import { shift, cmd } from 'lib/html-entities';
import CodeEditorFileExplorer from './CodeEditorFileExplorer';
import CodeEditorSearch from './CodeEditorSearch';
import useThemeColors from './utils/useThemeColors';
interface Props {
monaco: Monaco | undefined;
editor: monaco.editor.IStandaloneCodeEditor | undefined;
data: Array<File>;
onFileSelect: (index: number, lineNumber?: number) => void;
selectedFile: string;
......@@ -18,11 +22,12 @@ interface Props {
export const CONTAINER_WIDTH = 250;
const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props) => {
const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile }: Props) => {
const [ isStuck, setIsStuck ] = React.useState(false);
const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false);
const [ tabIndex, setTabIndex ] = React.useState(0);
const [ searchValue, setSearchValue ] = React.useState('');
const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => JSX.Element>();
const themeColors = useThemeColors();
......@@ -50,6 +55,39 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
onFileSelect(index, lineNumber);
}, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]);
React.useEffect(() => {
if (editor && monaco) {
editor.addAction({
id: 'file-explorer',
label: 'Show File Explorer',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyE,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
run: function() {
setTabIndex(0);
},
});
editor.addAction({
id: 'search-in-files',
label: 'Show Search in Files',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.6,
run: function(editor) {
setTabIndex(1);
const selection = editor.getSelection();
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : '';
setSearchValue(selectedValue || '');
},
});
}
}, [ editor, monaco ]);
return (
<>
<Box
......@@ -84,8 +122,8 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
boxShadow={ isStuck ? 'md' : 'none' }
borderTopRightRadius="md"
>
<Tab { ...tabProps }>Explorer</Tab>
<Tab { ...tabProps }>Search</Tab>
<Tab { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</Tab>
<Tab { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</Tab>
{ actionBarRenderer?.() }
</TabList>
<TabPanels>
......@@ -106,6 +144,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
isInputStuck={ isStuck }
isActive={ tabIndex === 1 }
setActionBarRenderer={ setActionBarRenderer }
defaultValue={ searchValue }
/>
</TabPanel>
</TabPanels>
......
import { Flex, Icon, chakra, Box } from '@chakra-ui/react';
import React from 'react';
import { alt } from 'lib/html-entities';
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
import iconFile from './icons/file.svg';
......@@ -62,7 +63,7 @@ const CodeEditorTab = ({ isActive, path, onClick, onClose, isCloseDisabled, tabs
boxSize="20px"
ml="4px"
p="2px"
title="Close"
title={ `Close ${ isActive ? `(${ alt }W)` : '' }` }
aria-label="Close"
onClick={ handleClose }
borderRadius="sm"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment