Skip to content

Commit

Permalink
feat: refactor CodeEditor (#360)
Browse files Browse the repository at this point in the history
* fix: attempt to fix eslint import recognition

* fix: fix lint issues

* feat: restructure code editor component

* feat: restructure CodeEditor component

* feat: use type safe connector for CodeEditor
  • Loading branch information
x1unix authored Jul 13, 2024
1 parent be436a8 commit e988e62
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 137 deletions.
9 changes: 8 additions & 1 deletion web/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true
Expand All @@ -21,7 +22,9 @@ module.exports = {
"prettier",
"unused-imports"
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
ecmaVersion: "latest",
ecmaFeatures: {
jsx: true
Expand Down Expand Up @@ -50,7 +53,11 @@ module.exports = {
},
'import/resolver': {
typescript: {
alwaysTryTypes: true
alwaysTryTypes: true,
project: ["**/tsconfig.json"]
},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@xterm/addon-canvas": "^0.6.0-beta.1",
"@xterm/addon-fit": "^0.9.0-beta.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const RunOutput: React.FC<StateProps & {}> = ({ status, monaco, terminal }) => {
}
}, [theme])
const fontFamily = useMemo(() => getFontFamily(monaco?.fontFamily ?? DEFAULT_FONT), [monaco])
const isClean = !status || !status?.dirty
const isClean = !status?.dirty

return (
<div className="RunOutput" style={styles}>
Expand Down
58 changes: 39 additions & 19 deletions web/src/components/features/workspace/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ import { Spinner } from '@fluentui/react'
import MonacoEditor, { type Monaco } from '@monaco-editor/react'
import { KeyMod, KeyCode, type editor, type IKeyboardEvent } from 'monaco-editor'

import apiClient from '~/services/api'
import { createVimModeAdapter, type StatusBarAdapter, type VimModeKeymap } from '~/plugins/vim/editor'
import { Analyzer } from '~/services/analyzer'
import { TargetType } from '~/services/config'
import { Connect, newMarkerAction, runFileDispatcher } from '~/store'
import { type MonacoSettings, TargetType } from '~/services/config'
import { connect, newMarkerAction, runFileDispatcher, type StateDispatch } from '~/store'
import { type WorkspaceState, dispatchFormatFile, dispatchResetWorkspace, dispatchUpdateFile } from '~/store/workspace'
import { getTimeNowUsageMarkers, wrapAsyncWithDebounce } from '../utils'
import { attachCustomCommands } from '../commands'
import { LANGUAGE_GOLANG, stateToOptions } from '../props'
import { getTimeNowUsageMarkers, wrapAsyncWithDebounce } from './utils'
import { attachCustomCommands } from './commands'
import { LANGUAGE_GOLANG, stateToOptions } from './props'
import { configureMonacoLoader } from './loader'
import { registerGoLanguageProvider } from './autocomplete'
import type { VimState } from '~/store/vim/state'

const ANALYZE_DEBOUNCE_TIME = 500

interface CodeEditorState {
code?: string
loading?: boolean
}
// ask monaco-editor/react to use our own Monaco instance.
configureMonacoLoader()

const mapWorkspaceProps = ({ files, selectedFile }: WorkspaceState) => {
if (!selectedFile) {
Expand All @@ -33,16 +35,22 @@ const mapWorkspaceProps = ({ files, selectedFile }: WorkspaceState) => {
}
}

@Connect(({ workspace, ...s }) => ({
...mapWorkspaceProps(workspace),
darkMode: s.settings.darkMode,
vimModeEnabled: s.settings.enableVimMode,
isServerEnvironment: s.runTarget.target === TargetType.Server,
loading: s.status?.loading,
options: s.monaco,
vim: s.vim,
}))
export class CodeEditor extends React.Component<any, CodeEditorState> {
interface CodeEditorState {
code?: string
loading?: boolean
}

interface Props extends CodeEditorState {
fileName: string
darkMode: boolean
vimModeEnabled: boolean
isServerEnvironment: boolean
options: MonacoSettings
vim?: VimState | null
dispatch: StateDispatch
}

class CodeEditor extends React.Component<Props> {
private analyzer?: Analyzer
private editorInstance?: editor.IStandaloneCodeEditor
private vimAdapter?: VimModeKeymap
Expand All @@ -57,6 +65,8 @@ export class CodeEditor extends React.Component<any, CodeEditorState> {
this.editorInstance = editorInstance
this.monaco = monacoInstance

registerGoLanguageProvider(apiClient)

editorInstance.onKeyDown((e) => this.onKeyDown(e))
const [vimAdapter, statusAdapter] = createVimModeAdapter(this.props.dispatch, editorInstance)
this.vimAdapter = vimAdapter
Expand Down Expand Up @@ -221,3 +231,13 @@ export class CodeEditor extends React.Component<any, CodeEditorState> {
)
}
}

export const ConnectedCodeEditor = connect<CodeEditorState, {}>(({ workspace, ...s }) => ({
...mapWorkspaceProps(workspace),
darkMode: s.settings.darkMode,
vimModeEnabled: s.settings.enableVimMode,
isServerEnvironment: s.runTarget.target === TargetType.Server,
loading: s.status?.loading,
options: s.monaco,
vim: s.vim,
}))(CodeEditor)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as monaco from 'monaco-editor'
import type { IAPIClient } from '~/services/api'

import { GoCompletionItemProvider } from './provider'

let alreadyRegistered = false
export const registerGoLanguageProvider = (client: IAPIClient) => {
if (alreadyRegistered) {
console.warn('Go Language provider was already registered')
return
}

alreadyRegistered = true
return monaco.languages.registerCompletionItemProvider('go', new GoCompletionItemProvider(client))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Matches package (and method name)
const COMPL_REGEXP = /([a-zA-Z0-9_]+)(\.([A-Za-z0-9_]+))?$/
const R_GROUP_PKG = 1
const R_GROUP_METHOD = 3

export const parseExpression = (expr: string) => {
COMPL_REGEXP.lastIndex = 0 // Reset regex state
const m = COMPL_REGEXP.exec(expr)
if (!m) {
return null
}

const varName = m[R_GROUP_PKG]
const propValue = m[R_GROUP_METHOD]

if (!propValue) {
return { value: varName }
}

return {
packageName: varName,
value: propValue,
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,12 @@
import { loader } from '@monaco-editor/react'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import * as monaco from 'monaco-editor'
import type * as monaco from 'monaco-editor'
import { type IAPIClient } from '~/services/api'
import { wrapAsyncWithDebounce } from '../utils'
import snippets from './snippets'
import { wrapAsyncWithDebounce } from './utils'
import { parseExpression } from './parse'

// Import aliases
type CompletionList = monaco.languages.CompletionList
type CompletionContext = monaco.languages.CompletionContext
type ITextModel = monaco.editor.ITextModel
type Position = monaco.Position
type CancellationToken = monaco.CancellationToken

let alreadyRegistered = false

// Matches package (and method name)
const COMPL_REGEXP = /([a-zA-Z0-9_]+)(\.([A-Za-z0-9_]+))?$/
const R_GROUP_PKG = 1
const R_GROUP_METHOD = 3
const SUGGESTIONS_DEBOUNCE_DELAY = 500

const parseExpression = (expr: string) => {
COMPL_REGEXP.lastIndex = 0 // Reset regex state
const m = COMPL_REGEXP.exec(expr)
if (!m) {
return null
}

const varName = m[R_GROUP_PKG]
const propValue = m[R_GROUP_METHOD]

if (!propValue) {
return { value: varName }
}

return {
packageName: varName,
value: propValue,
}
}

self.MonacoEnvironment = {
getWorker: () => {
return new EditorWorker()
},
}

class GoCompletionItemProvider implements monaco.languages.CompletionItemProvider {
export class GoCompletionItemProvider implements monaco.languages.CompletionItemProvider {
private readonly getSuggestionFunc: IAPIClient['getSuggestions']

constructor(private readonly client: IAPIClient) {
Expand All @@ -57,11 +17,11 @@ class GoCompletionItemProvider implements monaco.languages.CompletionItemProvide
}

async provideCompletionItems(
model: ITextModel,
position: Position,
context: CompletionContext,
token: CancellationToken,
): Promise<CompletionList> {
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken,
): Promise<monaco.languages.CompletionList> {
const val = model
.getValueInRange({
startLineNumber: position.lineNumber,
Expand Down Expand Up @@ -108,14 +68,3 @@ class GoCompletionItemProvider implements monaco.languages.CompletionItemProvide
}
}
}

export const registerGoLanguageProvider = (client: IAPIClient) => {
if (alreadyRegistered) {
console.warn('Go Language provider was already registered')
return
}

alreadyRegistered = true
loader.config({ monaco })
return monaco.languages.registerCompletionItemProvider('go', new GoCompletionItemProvider(client))
}
20 changes: 20 additions & 0 deletions web/src/components/features/workspace/CodeEditor/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { loader } from '@monaco-editor/react'
import * as monaco from 'monaco-editor'

import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'

let loaderConfigured = false
export const configureMonacoLoader = () => {
if (loaderConfigured) {
return
}

loader.config({ monaco })
loaderConfigured = true
}

self.MonacoEnvironment = {
getWorker: () => {
return new EditorWorker()
},
}
4 changes: 2 additions & 2 deletions web/src/components/features/workspace/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { TabView } from '~/components/elements/tabs/TabView'
import type { TabBarAction, TabIconStyles } from '~/components/elements/tabs/types'

import { CodeEditor } from '../CodeEditor'
import { ConnectedCodeEditor } from '../CodeEditor'
import { FlexContainer } from '../FlexContainer'
import { NewFileModal } from '../NewFileModal'
import { ContentPlaceholder } from '../ContentPlaceholder'
Expand Down Expand Up @@ -117,7 +117,7 @@ const Workspace: React.FC<Props> = ({ dispatch, files, selectedFile, snippet })
>
{tabs?.length ? (
<FlexContainer>
<CodeEditor />
<ConnectedCodeEditor />
</FlexContainer>
) : (
<ContentPlaceholder
Expand Down
3 changes: 0 additions & 3 deletions web/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { registerGoLanguageProvider } from '~/components/features/workspace/provider'
import apiClient from '~/services/api'
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
import { initializeIcons } from './icons'
import { App } from './App'
Expand All @@ -12,7 +10,6 @@ import 'core-js/actual/promise/all-settled'
import 'core-js/actual/array/flat-map'

initializeIcons()
registerGoLanguageProvider(apiClient)

// eslint-disable-next-line import/no-named-as-default-member
ReactDOM.render(<App />, document.getElementById('root'))
Expand Down
11 changes: 5 additions & 6 deletions web/src/plugins/vim/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { VimMode } from 'monaco-vim'
import VimModeKeymap from 'monaco-vim/lib/cm/keymap_vim'
import type { Nullable } from '~/utils/types'

import { runFileDispatcher } from '~/store'
import { runFileDispatcher, type StateDispatch } from '~/store'
import { dispatchShareSnippet } from '~/store/workspace'
import { type Dispatch } from '~/store/vim/state'
import {
newVimCommandDoneAction,
newVimCommandStartAction,
Expand Down Expand Up @@ -53,7 +52,7 @@ class VimModeKeymapAdapter extends VimModeKeymap {

constructor(
// "dispatch" is reserved method in inner class.
private readonly dispatchFunc: Dispatch,
private readonly dispatchFunc: StateDispatch,
editorInstance: editor.IStandaloneCodeEditor,
) {
super(editorInstance)
Expand Down Expand Up @@ -89,7 +88,7 @@ export class StatusBarAdapter {
private currentOpts?: Nullable<CommandInputOpts>

constructor(
private readonly dispatchFn: Dispatch,
private readonly dispatchFn: StateDispatch,
private readonly editor: editor.IStandaloneCodeEditor,
) {}

Expand Down Expand Up @@ -156,7 +155,7 @@ export class StatusBarAdapter {
* @param e
* @param currentData
*/
handleKeyDownEvent(e: IKeyboardEvent, currentData: string) {
handleKeyDownEvent(e: IKeyboardEvent, currentData: string = '') {
e.preventDefault()
e.stopPropagation()

Expand Down Expand Up @@ -192,7 +191,7 @@ export class StatusBarAdapter {
* @param editorInstance Monaco editor instance
*/
export const createVimModeAdapter = (
dispatch: Dispatch,
dispatch: StateDispatch,
editorInstance: editor.IStandaloneCodeEditor,
): [VimModeKeymap, StatusBarAdapter] => {
const vimAdapter: VimModeKeymap = new VimModeKeymapAdapter(dispatch, editorInstance)
Expand Down
4 changes: 4 additions & 0 deletions web/src/store/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export interface State {
terminal: TerminalState
}

/**
* Connect decorator to attach component to store.
* @deprecated use `connect` instead.
*/
export function Connect(fn: (state: State) => any) {
return function (constructor: Function) {
return connect(fn)(constructor as any) as any
Expand Down
Loading

0 comments on commit e988e62

Please sign in to comment.