Commit f03c435c authored by tom's avatar tom

hotkeys for code editor

parent 9dfd4a93
...@@ -19,3 +19,6 @@ export const minus = String.fromCharCode(8722); // − ...@@ -19,3 +19,6 @@ export const minus = String.fromCharCode(8722); // −
export const leftLineArrow = String.fromCharCode(8592); // ← export const leftLineArrow = String.fromCharCode(8592); // ←
export const rightLineArrow = String.fromCharCode(8594); // → export const rightLineArrow = String.fromCharCode(8594); // →
export const apos = String.fromCharCode(39); // apostrophe ' 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 { ...@@ -39,11 +39,11 @@ interface Props {
const CodeEditor = ({ data }: Props) => { const CodeEditor = ({ data }: Props) => {
const [ instance, setInstance ] = React.useState<Monaco | undefined>(); const [ instance, setInstance ] = React.useState<Monaco | undefined>();
const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>();
const [ index, setIndex ] = React.useState(0); const [ index, setIndex ] = React.useState(0);
const [ tabs, setTabs ] = React.useState([ data[index].file_path ]); const [ tabs, setTabs ] = React.useState([ data[index].file_path ]);
const [ isMetaPressed, setIsMetaPressed ] = React.useState(false); const [ isMetaPressed, setIsMetaPressed ] = React.useState(false);
const editorRef = React.useRef<monaco.editor.IStandaloneCodeEditor>();
const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>(); const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
...@@ -58,7 +58,7 @@ const CodeEditor = ({ data }: Props) => { ...@@ -58,7 +58,7 @@ const CodeEditor = ({ data }: Props) => {
const handleEditorDidMount = React.useCallback((editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => { const handleEditorDidMount = React.useCallback((editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
setInstance(monaco); setInstance(monaco);
editorRef.current = editor; setEditor(editor);
monaco.editor.defineTheme('blockscout-light', themes.light); monaco.editor.defineTheme('blockscout-light', themes.light);
monaco.editor.defineTheme('blockscout-dark', themes.dark); monaco.editor.defineTheme('blockscout-dark', themes.dark);
...@@ -71,6 +71,23 @@ const CodeEditor = ({ data }: Props) => { ...@@ -71,6 +71,23 @@ const CodeEditor = ({ data }: Props) => {
.map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path))); .map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path)));
loadedModels.concat(newModels).forEach(addFileImportDecorations); 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 // componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]); }, [ ]);
...@@ -80,11 +97,11 @@ const CodeEditor = ({ data }: Props) => { ...@@ -80,11 +97,11 @@ const CodeEditor = ({ data }: Props) => {
setTabs((prev) => prev.some((item) => item === data[index].file_path) ? prev : ([ ...prev, data[index].file_path ])); setTabs((prev) => prev.some((item) => item === data[index].file_path) ? prev : ([ ...prev, data[index].file_path ]));
if (lineNumber !== undefined && !Object.is(lineNumber, NaN)) { if (lineNumber !== undefined && !Object.is(lineNumber, NaN)) {
window.setTimeout(() => { window.setTimeout(() => {
editorRef.current?.revealLineInCenter(lineNumber); editor?.revealLineInCenter(lineNumber);
}, 0); }, 0);
} }
editorRef.current?.focus(); editor?.focus();
}, [ data ]); }, [ data, editor ]);
const handleTabSelect = React.useCallback((path: string) => { const handleTabSelect = React.useCallback((path: string) => {
const index = data.findIndex((item) => item.file_path === path); const index = data.findIndex((item) => item.file_path === path);
...@@ -93,14 +110,14 @@ const CodeEditor = ({ data }: Props) => { ...@@ -93,14 +110,14 @@ const CodeEditor = ({ data }: Props) => {
} }
}, [ data ]); }, [ data ]);
const handleTabClose = React.useCallback((path: string) => { const handleTabClose = React.useCallback((path: string, _isActive?: boolean) => {
setTabs((prev) => { setTabs((prev) => {
if (prev.length > 1) { if (prev.length > 1) {
const tabIndex = prev.findIndex((item) => item === path); 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) { 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); setIndex(nextActiveIndex);
} }
...@@ -200,7 +217,13 @@ const CodeEditor = ({ data }: Props) => { ...@@ -200,7 +217,13 @@ const CodeEditor = ({ data }: Props) => {
loading={ <CodeEditorLoading/> } loading={ <CodeEditorLoading/> }
/> />
</Box> </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> </Flex>
); );
}; };
......
...@@ -18,9 +18,10 @@ interface Props { ...@@ -18,9 +18,10 @@ interface Props {
isInputStuck: boolean; isInputStuck: boolean;
isActive: boolean; isActive: boolean;
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>; 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 [ searchTerm, changeSearchTerm ] = React.useState('');
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]); const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]); const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
...@@ -33,6 +34,10 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -33,6 +34,10 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
const debouncedSearchTerm = useDebounce(searchTerm, 300); const debouncedSearchTerm = useDebounce(searchTerm, 300);
React.useEffect(() => {
changeSearchTerm(defaultValue);
}, [ defaultValue ]);
React.useEffect(() => { React.useEffect(() => {
if (!monaco) { if (!monaco) {
return; return;
...@@ -166,7 +171,8 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -166,7 +171,8 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
borderWidth="1px" borderWidth="1px"
borderColor={ themeColors['input.background'] } borderColor={ themeColors['input.background'] }
py="2px" py="2px"
px="4px" pl="4px"
pr="75px"
transitionDuration="0" transitionDuration="0"
_focus={{ _focus={{
borderColor: themeColors.focusBorder, borderColor: themeColors.focusBorder,
......
import type { HTMLChakraProps } from '@chakra-ui/react'; import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react'; import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
import _throttle from 'lodash/throttle'; import _throttle from 'lodash/throttle';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react'; import React from 'react';
import type { File, Monaco } from './types'; import type { File, Monaco } from './types';
import { shift, cmd } from 'lib/html-entities';
import CodeEditorFileExplorer from './CodeEditorFileExplorer'; import CodeEditorFileExplorer from './CodeEditorFileExplorer';
import CodeEditorSearch from './CodeEditorSearch'; import CodeEditorSearch from './CodeEditorSearch';
import useThemeColors from './utils/useThemeColors'; import useThemeColors from './utils/useThemeColors';
interface Props { interface Props {
monaco: Monaco | undefined; monaco: Monaco | undefined;
editor: monaco.editor.IStandaloneCodeEditor | undefined;
data: Array<File>; data: Array<File>;
onFileSelect: (index: number, lineNumber?: number) => void; onFileSelect: (index: number, lineNumber?: number) => void;
selectedFile: string; selectedFile: string;
...@@ -18,11 +22,12 @@ interface Props { ...@@ -18,11 +22,12 @@ interface Props {
export const CONTAINER_WIDTH = 250; 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 [ isStuck, setIsStuck ] = React.useState(false);
const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false); const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false);
const [ tabIndex, setTabIndex ] = React.useState(0); const [ tabIndex, setTabIndex ] = React.useState(0);
const [ searchValue, setSearchValue ] = React.useState('');
const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => JSX.Element>(); const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => JSX.Element>();
const themeColors = useThemeColors(); const themeColors = useThemeColors();
...@@ -50,6 +55,39 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props) ...@@ -50,6 +55,39 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
onFileSelect(index, lineNumber); onFileSelect(index, lineNumber);
}, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]); }, [ 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 ( return (
<> <>
<Box <Box
...@@ -84,8 +122,8 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props) ...@@ -84,8 +122,8 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
boxShadow={ isStuck ? 'md' : 'none' } boxShadow={ isStuck ? 'md' : 'none' }
borderTopRightRadius="md" borderTopRightRadius="md"
> >
<Tab { ...tabProps }>Explorer</Tab> <Tab { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</Tab>
<Tab { ...tabProps }>Search</Tab> <Tab { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</Tab>
{ actionBarRenderer?.() } { actionBarRenderer?.() }
</TabList> </TabList>
<TabPanels> <TabPanels>
...@@ -106,6 +144,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props) ...@@ -106,6 +144,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, selectedFile }: Props)
isInputStuck={ isStuck } isInputStuck={ isStuck }
isActive={ tabIndex === 1 } isActive={ tabIndex === 1 }
setActionBarRenderer={ setActionBarRenderer } setActionBarRenderer={ setActionBarRenderer }
defaultValue={ searchValue }
/> />
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
......
import { Flex, Icon, chakra, Box } from '@chakra-ui/react'; import { Flex, Icon, chakra, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { alt } from 'lib/html-entities';
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors'; import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
import iconFile from './icons/file.svg'; import iconFile from './icons/file.svg';
...@@ -62,7 +63,7 @@ const CodeEditorTab = ({ isActive, path, onClick, onClose, isCloseDisabled, tabs ...@@ -62,7 +63,7 @@ const CodeEditorTab = ({ isActive, path, onClick, onClose, isCloseDisabled, tabs
boxSize="20px" boxSize="20px"
ml="4px" ml="4px"
p="2px" p="2px"
title="Close" title={ `Close ${ isActive ? `(${ alt }W)` : '' }` }
aria-label="Close" aria-label="Close"
onClick={ handleClose } onClick={ handleClose }
borderRadius="sm" 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