Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vim mode #169

Merged
merged 15 commits into from
May 10, 2022
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
"file-saver": "^2.0.5",
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"monaco-vim": "^0.3.4",
"re-resizable": "^6.9.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
16 changes: 9 additions & 7 deletions web/src/components/core/StatusBar/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import {editor} from "monaco-editor";
import {editor} from 'monaco-editor';
import {newEnvironmentChangeAction} from '~/store';
import config, {RuntimeType} from '~/services/config';
import EllipsisText from '~/components/utils/EllipsisText';
import StatusBarItem from '~/components/core/StatusBar/StatusBarItem';
import EnvironmentSelectModal from '~/components/modals/EnvironmentSelectModal';
import {newEnvironmentChangeAction} from "~/store";
import VimStatusBarItem from '~/plugins/vim/VimStatusBarItem';
import './StatusBar.css';

interface Props {
@@ -19,7 +20,7 @@ interface Props {
const getStatusItem = ({loading, lastError}) => {
if (loading) {
return (
<StatusBarItem iconName="Build">
<StatusBarItem icon="Build">
<EllipsisText>
Loading
</EllipsisText>
@@ -30,7 +31,7 @@ const getStatusItem = ({loading, lastError}) => {
if (lastError) {
return (
<StatusBarItem
iconName="NotExecuted"
icon="NotExecuted"
hideTextOnMobile
disabled
>
@@ -51,17 +52,18 @@ const StatusBar: React.FC<Props> = ({
<div className={className}>
<div className="StatusBar__side-left">
<StatusBarItem
iconName="ErrorBadge"
icon="ErrorBadge"
title="No Problems"
button
>
{markers?.length ?? 0} Errors
</StatusBarItem>
{getStatusItem({loading, lastError})}
<VimStatusBarItem />
</div>
<div className="StatusBar__side-right">
<StatusBarItem
iconName="Code"
icon="Code"
title="Select environment"
disabled={loading}
onClick={() => setRunSelectorModalVisible(true)}
@@ -71,7 +73,7 @@ const StatusBar: React.FC<Props> = ({
{RuntimeType.toString(runtime)}
</StatusBarItem>
<StatusBarItem
iconName="Feedback"
icon="Feedback"
title="Send feedback"
href={config.issueUrl}
iconOnly
41 changes: 31 additions & 10 deletions web/src/components/core/StatusBar/StatusBarItem.tsx
Original file line number Diff line number Diff line change
@@ -2,25 +2,34 @@ import React, {CSSProperties, MouseEventHandler} from 'react';
import { FontIcon } from '@fluentui/react/lib/Icon';
import './StatusBarItem.css';

interface Props {
iconName?: string
export interface StatusBarItemProps {
icon?: string | React.ComponentType,
iconOnly?: boolean
imageSrc?: string
button?: boolean
disabled?: boolean
hidden?: boolean
hideTextOnMobile?: boolean
href?: string
title?: string
onClick?: MouseEventHandler<HTMLButtonElement|HTMLAnchorElement>
style?: CSSProperties
}

const getItemContents = ({iconName, iconOnly, imageSrc, title, children}) => (
const getIcon = (icon: string | React.ComponentType) => (
typeof icon === 'string' ? (
<FontIcon iconName={icon} className="StatusBarItem__icon" />
) : (
React.createElement<any>(icon as React.ComponentType, {
className: 'StatusBarItem__icon'
})
)
)

const getItemContents = ({icon, iconOnly, imageSrc, title, children}) => (
<>
{
iconName && (
<FontIcon iconName={iconName} className="StatusBarItem__icon"/>
)
icon && getIcon(icon)
}
{
imageSrc && (
@@ -37,11 +46,23 @@ const getItemContents = ({iconName, iconOnly, imageSrc, title, children}) => (
</>
)

const StatusBarItem: React.FC<Props> = ({
title, iconName, iconOnly, imageSrc, hideTextOnMobile,
href, button, children, ...props
const StatusBarItem: React.FC<StatusBarItemProps> = ({
title,
icon,
iconOnly,
imageSrc,
hideTextOnMobile,
href,
button,
children,
hidden,
...props
}) => {
const content = getItemContents({iconName, iconOnly, children, imageSrc, title});
if (hidden) {
return null;
}

const content = getItemContents({icon, iconOnly, children, imageSrc, title});
const className = hideTextOnMobile ? (
'StatusBarItem StatusBarItem--hideOnMobile'
) : 'StatusBarItem';
78 changes: 63 additions & 15 deletions web/src/components/editor/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React from 'react';
import MonacoEditor from 'react-monaco-editor';
import { editor } from 'monaco-editor';
import {editor, IKeyboardEvent} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import { attachCustomCommands } from '@components/editor/commands';
import {
VimModeKeymap,
createVimModeAdapter,
StatusBarAdapter
} from '~/plugins/vim/editor';
import { attachCustomCommands } from './commands';

import {
Connect,
@@ -13,7 +18,6 @@ import {
newMarkerAction
} from '~/store';
import { Analyzer } from '~/services/analyzer';

import { LANGUAGE_GOLANG, stateToOptions } from './props';

const ANALYZE_DEBOUNCE_TIME = 500;
@@ -26,16 +30,33 @@ interface CodeEditorState {
@Connect(s => ({
code: s.editor.code,
darkMode: s.settings.darkMode,
vimModeEnabled: s.settings.enableVimMode,
loading: s.status?.loading,
options: s.monaco,
vim: s.vim,
}))
export default class CodeEditor extends React.Component<any, CodeEditorState> {
analyzer?: Analyzer;
_previousTimeout: any;
editorInstance?: editor.IStandaloneCodeEditor;
private analyzer?: Analyzer;
private _previousTimeout: any;
private editorInstance?: editor.IStandaloneCodeEditor;
private vimAdapter?: VimModeKeymap;
private vimCommandAdapter?: StatusBarAdapter;

editorDidMount(editorInstance: editor.IStandaloneCodeEditor, _: monaco.editor.IEditorConstructionOptions) {
this.editorInstance = editorInstance;
editorInstance.onKeyDown(e => this.onKeyDown(e));
const [ vimAdapter, statusAdapter ] = createVimModeAdapter(
this.props.dispatch,
editorInstance
);
this.vimAdapter = vimAdapter;
this.vimCommandAdapter = statusAdapter;

if (this.props.vimModeEnabled) {
console.log('Vim mode enabled');
this.vimAdapter.attach();
}

if (Analyzer.supported()) {
this.analyzer = new Analyzer();
} else {
@@ -81,11 +102,27 @@ export default class CodeEditor extends React.Component<any, CodeEditorState> {
editorInstance.focus();
}

componentDidUpdate(prevProps) {
if (prevProps?.vimModeEnabled === this.props.vimModeEnabled) {
return
}

if (this.props.vimModeEnabled) {
console.log('Vim mode enabled');
this.vimAdapter?.attach();
return;
}

console.log('Vim mode disabled');
this.vimAdapter?.dispose();
}

componentWillUnmount() {
this.analyzer?.dispose();
this.vimAdapter?.dispose();
}

onChange(newValue: string, e: editor.IModelContentChangedEvent) {
onChange(newValue: string, _: editor.IModelContentChangedEvent) {
this.props.dispatch(newFileChangeAction(newValue));

if (this.analyzer) {
@@ -111,15 +148,26 @@ export default class CodeEditor extends React.Component<any, CodeEditorState> {
}, ANALYZE_DEBOUNCE_TIME);
}

private onKeyDown(e: IKeyboardEvent) {
const {vimModeEnabled, vim} = this.props;
if (!vimModeEnabled || !vim?.commandStarted) {
return;
}

this.vimCommandAdapter?.handleKeyDownEvent(e, vim?.keyBuffer);
}

render() {
const options = stateToOptions(this.props.options);
return <MonacoEditor
language={LANGUAGE_GOLANG}
theme={this.props.darkMode ? 'vs-dark' : 'vs-light'}
value={this.props.code}
options={options}
onChange={(newVal, e) => this.onChange(newVal, e)}
editorDidMount={(e, m: any) => this.editorDidMount(e, m)}
/>;
return (
<MonacoEditor
language={LANGUAGE_GOLANG}
theme={this.props.darkMode ? 'vs-dark' : 'vs-light'}
value={this.props.code}
options={options}
onChange={(newVal, e) => this.onChange(newVal, e)}
editorDidMount={(e, m: any) => this.editorDidMount(e, m)}
/>
);
}
}
Loading