291 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			291 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import React, { useEffect, useRef } from 'react'
 | 
						|
import { useSnapshot, ref } from 'valtio'
 | 
						|
import type monaco from 'monaco-editor'
 | 
						|
import { ArrowBendLeftUp } from 'phosphor-react'
 | 
						|
import { useTheme } from 'next-themes'
 | 
						|
import { useRouter } from 'next/router'
 | 
						|
 | 
						|
import Box from './Box'
 | 
						|
import Container from './Container'
 | 
						|
import { createNewFile, saveFile } from '../state/actions'
 | 
						|
import { apiHeaderFiles } from '../state/constants'
 | 
						|
import state from '../state'
 | 
						|
 | 
						|
import EditorNavigation from './EditorNavigation'
 | 
						|
import Text from './Text'
 | 
						|
import { MonacoServices } from '@codingame/monaco-languageclient'
 | 
						|
import { createLanguageClient, createWebSocket } from '../utils/languageClient'
 | 
						|
import { listen } from '@codingame/monaco-jsonrpc'
 | 
						|
import ReconnectingWebSocket from 'reconnecting-websocket'
 | 
						|
 | 
						|
import docs from '../xrpl-hooks-docs/docs'
 | 
						|
import Monaco from './Monaco'
 | 
						|
import { saveAllFiles } from '../state/actions/saveFile'
 | 
						|
import { Tab, Tabs } from './Tabs'
 | 
						|
import { renameFile } from '../state/actions/createNewFile'
 | 
						|
 | 
						|
const checkWritable = (filename?: string): boolean => {
 | 
						|
  if (apiHeaderFiles.find(file => file === filename)) {
 | 
						|
    return false
 | 
						|
  }
 | 
						|
  return true
 | 
						|
}
 | 
						|
 | 
						|
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
						|
  const filename = editor.getModel()?.uri.path.split('/').pop()
 | 
						|
  const isWritable = checkWritable(filename)
 | 
						|
  editor.updateOptions({ readOnly: !isWritable })
 | 
						|
}
 | 
						|
 | 
						|
let decorations: { [key: string]: string[] } = {}
 | 
						|
 | 
						|
const setMarkers = (monacoE: typeof monaco) => {
 | 
						|
  // Get all the markers that are active at the moment,
 | 
						|
  // Also if same error is there twice, we can show the content
 | 
						|
  // only once (that's why we're using uniqBy)
 | 
						|
  const markers = monacoE.editor
 | 
						|
    .getModelMarkers({})
 | 
						|
    // Filter out the markers that are hooks specific
 | 
						|
    .filter(
 | 
						|
      marker =>
 | 
						|
        typeof marker?.code === 'string' &&
 | 
						|
        // Take only markers that starts with "hooks-"
 | 
						|
        marker?.code?.includes('hooks-')
 | 
						|
    )
 | 
						|
 | 
						|
  // Get the active model (aka active file you're editing)
 | 
						|
  // const model = monacoE.editor?.getModel(
 | 
						|
  //   monacoE.Uri.parse(`file:///work/c/${state.files?.[state.active]?.name}`)
 | 
						|
  // );
 | 
						|
  // console.log(state.active);
 | 
						|
  // Add decoration (aka extra hoverMessages) to markers in the
 | 
						|
  // exact same range (location) where the markers are
 | 
						|
  const models = monacoE.editor.getModels()
 | 
						|
  models.forEach(model => {
 | 
						|
    decorations[model.id] = model?.deltaDecorations(
 | 
						|
      decorations?.[model.id] || [],
 | 
						|
      markers
 | 
						|
        .filter(marker =>
 | 
						|
          marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
 | 
						|
        )
 | 
						|
        .map(marker => ({
 | 
						|
          range: new monacoE.Range(
 | 
						|
            marker.startLineNumber,
 | 
						|
            marker.startColumn,
 | 
						|
            marker.endLineNumber,
 | 
						|
            marker.endColumn
 | 
						|
          ),
 | 
						|
          options: {
 | 
						|
            hoverMessage: {
 | 
						|
              value:
 | 
						|
                // Find the related hover message markdown from the
 | 
						|
                // /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
 | 
						|
                // which was generated from rst files
 | 
						|
 | 
						|
                (typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
 | 
						|
              supportHtml: true,
 | 
						|
              isTrusted: true
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }))
 | 
						|
    )
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
const HooksEditor = () => {
 | 
						|
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
 | 
						|
  const monacoRef = useRef<typeof monaco>()
 | 
						|
  const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
 | 
						|
  const snap = useSnapshot(state)
 | 
						|
  const router = useRouter()
 | 
						|
  const { theme } = useTheme()
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    if (editorRef.current) validateWritability(editorRef.current)
 | 
						|
  }, [snap.active])
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    return () => {
 | 
						|
      subscriptionRef?.current?.close()
 | 
						|
    }
 | 
						|
  }, [])
 | 
						|
  useEffect(() => {
 | 
						|
    if (monacoRef.current) {
 | 
						|
      setMarkers(monacoRef.current)
 | 
						|
    }
 | 
						|
  }, [snap.active])
 | 
						|
  useEffect(() => {
 | 
						|
    return () => {
 | 
						|
      saveAllFiles()
 | 
						|
    }
 | 
						|
  }, [])
 | 
						|
 | 
						|
  const file = snap.files[snap.active]
 | 
						|
 | 
						|
  const renderNav = () => (
 | 
						|
    <Tabs
 | 
						|
      label="File"
 | 
						|
      activeIndex={snap.active}
 | 
						|
      onChangeActive={idx => (state.active = idx)}
 | 
						|
      extensionRequired
 | 
						|
      onCreateNewTab={createNewFile}
 | 
						|
      onCloseTab={idx => state.files.splice(idx, 1)}
 | 
						|
      onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
 | 
						|
      headerExtraValidation={{
 | 
						|
        regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
 | 
						|
        error: 'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"'
 | 
						|
      }}
 | 
						|
    >
 | 
						|
      {snap.files.map((file, index) => {
 | 
						|
        return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />
 | 
						|
      })}
 | 
						|
    </Tabs>
 | 
						|
  )
 | 
						|
  return (
 | 
						|
    <Box
 | 
						|
      css={{
 | 
						|
        flex: 1,
 | 
						|
        flexShrink: 1,
 | 
						|
        display: 'flex',
 | 
						|
        position: 'relative',
 | 
						|
        flexDirection: 'column',
 | 
						|
        backgroundColor: '$mauve2',
 | 
						|
        width: '100%'
 | 
						|
      }}
 | 
						|
    >
 | 
						|
      <EditorNavigation renderNav={renderNav} />
 | 
						|
      {snap.files.length > 0 && router.isReady ? (
 | 
						|
        <Monaco
 | 
						|
          keepCurrentModel
 | 
						|
          defaultLanguage={file?.language}
 | 
						|
          language={file?.language}
 | 
						|
          path={`file:///work/c/${file?.name}`}
 | 
						|
          defaultValue={file?.content}
 | 
						|
          // onChange={val => (state.files[snap.active].content = val)} // Auto save?
 | 
						|
          beforeMount={monaco => {
 | 
						|
            if (!snap.editorCtx) {
 | 
						|
              snap.files.forEach(file =>
 | 
						|
                monaco.editor.createModel(
 | 
						|
                  file.content,
 | 
						|
                  file.language,
 | 
						|
                  monaco.Uri.parse(`file:///work/c/${file.name}`)
 | 
						|
                )
 | 
						|
              )
 | 
						|
            }
 | 
						|
 | 
						|
            // create the web socket
 | 
						|
            if (!subscriptionRef.current) {
 | 
						|
              monaco.languages.register({
 | 
						|
                id: 'c',
 | 
						|
                extensions: ['.c', '.h'],
 | 
						|
                aliases: ['C', 'c', 'H', 'h'],
 | 
						|
                mimetypes: ['text/plain']
 | 
						|
              })
 | 
						|
              MonacoServices.install(monaco)
 | 
						|
              const webSocket = createWebSocket(
 | 
						|
                process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
 | 
						|
              )
 | 
						|
              subscriptionRef.current = webSocket
 | 
						|
              // listen when the web socket is opened
 | 
						|
              listen({
 | 
						|
                webSocket: webSocket as WebSocket,
 | 
						|
                onConnection: connection => {
 | 
						|
                  // create and start the language client
 | 
						|
                  const languageClient = createLanguageClient(connection)
 | 
						|
                  const disposable = languageClient.start()
 | 
						|
 | 
						|
                  connection.onClose(() => {
 | 
						|
                    try {
 | 
						|
                      disposable.dispose()
 | 
						|
                    } catch (err) {
 | 
						|
                      console.log('err', err)
 | 
						|
                    }
 | 
						|
                  })
 | 
						|
                }
 | 
						|
              })
 | 
						|
            }
 | 
						|
 | 
						|
            // editor.updateOptions({
 | 
						|
            //   minimap: {
 | 
						|
            //     enabled: false,
 | 
						|
            //   },
 | 
						|
            //   ...snap.editorSettings,
 | 
						|
            // });
 | 
						|
            if (!state.editorCtx) {
 | 
						|
              state.editorCtx = ref(monaco.editor)
 | 
						|
            }
 | 
						|
          }}
 | 
						|
          onMount={(editor, monaco) => {
 | 
						|
            editorRef.current = editor
 | 
						|
            monacoRef.current = monaco
 | 
						|
            editor.updateOptions({
 | 
						|
              glyphMargin: true,
 | 
						|
              lightbulb: {
 | 
						|
                enabled: true
 | 
						|
              }
 | 
						|
            })
 | 
						|
            editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
 | 
						|
              saveFile()
 | 
						|
            })
 | 
						|
            // When the markers (errors/warnings from clangd language server) change
 | 
						|
            // Lets improve the markers by adding extra content to them from related
 | 
						|
            // md files
 | 
						|
            monaco.editor.onDidChangeMarkers(() => {
 | 
						|
              if (monacoRef.current) {
 | 
						|
                setMarkers(monacoRef.current)
 | 
						|
              }
 | 
						|
            })
 | 
						|
 | 
						|
            // Hacky way to hide Peek menu
 | 
						|
            editor.onContextMenu(e => {
 | 
						|
              const host = document.querySelector<HTMLElement>('.shadow-root-host')
 | 
						|
 | 
						|
              const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
 | 
						|
              contextMenuItems?.forEach(k => {
 | 
						|
                // If menu item contains "Peek" lets hide it
 | 
						|
                if (k.querySelector('.action-label')?.textContent === 'Peek') {
 | 
						|
                  // @ts-expect-error
 | 
						|
                  k['style'].display = 'none'
 | 
						|
                }
 | 
						|
              })
 | 
						|
            })
 | 
						|
 | 
						|
            validateWritability(editor)
 | 
						|
          }}
 | 
						|
          theme={theme === 'dark' ? 'dark' : 'light'}
 | 
						|
        />
 | 
						|
      ) : (
 | 
						|
        <Container>
 | 
						|
          {!snap.loading && router.isReady && (
 | 
						|
            <Box
 | 
						|
              css={{
 | 
						|
                flexDirection: 'row',
 | 
						|
                width: '$spaces$wide',
 | 
						|
                gap: '$3',
 | 
						|
                display: 'inline-flex'
 | 
						|
              }}
 | 
						|
            >
 | 
						|
              <Box css={{ display: 'inline-flex', pl: '35px' }}>
 | 
						|
                <ArrowBendLeftUp size={30} />
 | 
						|
              </Box>
 | 
						|
              <Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
 | 
						|
                <Text
 | 
						|
                  css={{
 | 
						|
                    fontSize: '14px',
 | 
						|
                    maxWidth: '220px',
 | 
						|
                    fontFamily: '$monospace'
 | 
						|
                  }}
 | 
						|
                >
 | 
						|
                  Click the link above to create your file
 | 
						|
                </Text>
 | 
						|
              </Box>
 | 
						|
            </Box>
 | 
						|
          )}
 | 
						|
        </Container>
 | 
						|
      )}
 | 
						|
    </Box>
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
export default HooksEditor
 |