From e22f4b9c34cd7d73500295c2526b195742e17f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ctan-nhu=E2=80=9D?= <“tan@harness.io”> Date: Thu, 6 Apr 2023 15:48:58 -0700 Subject: [PATCH] Create Editor component to wrap CodeMirror 6 --- web/src/components/Editor/Editor.tsx | 91 ++++++++++++++ .../FileContent/GitBlame.tsx | 114 ++++++------------ 2 files changed, 125 insertions(+), 80 deletions(-) create mode 100644 web/src/components/Editor/Editor.tsx diff --git a/web/src/components/Editor/Editor.tsx b/web/src/components/Editor/Editor.tsx new file mode 100644 index 000000000..832628c7a --- /dev/null +++ b/web/src/components/Editor/Editor.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useMemo, useRef } from 'react' +import { Container } from '@harness/uicore' +import { LanguageDescription } from '@codemirror/language' +import { indentWithTab } from '@codemirror/commands' +import type { ViewUpdate } from '@codemirror/view' +import { languages } from '@codemirror/language-data' +import { EditorView, keymap } from '@codemirror/view' +import { noop } from 'lodash-es' +import { Compartment, EditorState, Extension } from '@codemirror/state' +import { color } from '@uiw/codemirror-extensions-color' +import { hyperLink } from '@uiw/codemirror-extensions-hyper-link' +import { githubLight as theme } from '@uiw/codemirror-themes-all' + +interface EditorProps { + filename: string + source: string + onViewUpdate?: (update: ViewUpdate) => void + readonly?: boolean + className?: string + extensions?: Extension + viewRef?: React.MutableRefObject +} + +export const Editor = React.memo(function GitBlameSourceViewer({ + source, + filename, + onViewUpdate = noop, + readonly = false, + className, + extensions = new Compartment().of([]), + viewRef +}: EditorProps) { + const view = useRef() + const ref = useRef() + const languageConfig = useMemo(() => new Compartment(), []) + + useEffect(() => { + const editorView = new EditorView({ + doc: source, + extensions: [ + extensions, + + color, + hyperLink, + theme, + + EditorView.lineWrapping, + keymap.of([indentWithTab]), + + ...(readonly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []), + + EditorView.updateListener.of(onViewUpdate), + + /** + languageConfig is a compartment that defaults to an empty array (no language support) + at first, when a language is detected, languageConfig is used to reconfigure dynamically. + @see https://codemirror.net/examples/config/ + */ + languageConfig.of([]) + ], + parent: ref.current + }) + + view.current = editorView + + if (viewRef) { + viewRef.current = editorView + } + + return () => { + editorView.destroy() + } + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + // Dynamically load language support based on filename + useEffect(() => { + if (filename) { + languageDescriptionFrom(filename) + ?.load() + .then(languageSupport => { + view.current?.dispatch({ effects: languageConfig.reconfigure(languageSupport) }) + }) + } + }, [filename, view, languageConfig]) + + return +}) + +function languageDescriptionFrom(filename: string) { + return LanguageDescription.matchFilename(languages, filename) +} diff --git a/web/src/pages/Repository/RepositoryContent/FileContent/GitBlame.tsx b/web/src/pages/Repository/RepositoryContent/FileContent/GitBlame.tsx index bd47bc123..9251644a9 100644 --- a/web/src/pages/Repository/RepositoryContent/FileContent/GitBlame.tsx +++ b/web/src/pages/Repository/RepositoryContent/FileContent/GitBlame.tsx @@ -1,21 +1,17 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Avatar, Container, FontVariation, Layout, StringSubstitute, Text } from '@harness/uicore' -import { LanguageDescription } from '@codemirror/language' -import { indentWithTab } from '@codemirror/commands' -import { ViewPlugin, ViewUpdate } from '@codemirror/view' -import { languages } from '@codemirror/language-data' -import { EditorView, gutter, GutterMarker, keymap, WidgetType } from '@codemirror/view' -import { Compartment, EditorState } from '@codemirror/state' +import type { ViewUpdate } from '@codemirror/view' +import { EditorView, gutter, GutterMarker, WidgetType } from '@codemirror/view' +import { Compartment } from '@codemirror/state' import ReactTimeago from 'react-timeago' -import { color } from '@uiw/codemirror-extensions-color' -import { hyperLink } from '@uiw/codemirror-extensions-hyper-link' -import { githubLight as theme } from '@uiw/codemirror-themes-all' import { useGet } from 'restful-react' import { Render } from 'react-jsx-match' +import { noop } from 'lodash-es' import type { GitrpcBlamePart } from 'services/code' import type { GitInfoProps } from 'utils/GitUtils' import { useStrings } from 'framework/strings' import { getErrorMessage } from 'utils/Utils' +import { Editor } from 'components/Editor/Editor' import { lineWidget, LineWidgetPosition, LineWidgetSpec } from './lineWidget' import css from './GitBlame.module.scss' @@ -151,7 +147,7 @@ export const GitBlame: React.FC - (lines as string[]).join('\n')).join('\n') || ''} filename={resourcePath} onViewUpdate={onViewUpdate} @@ -179,7 +175,7 @@ class CustomLineNumber extends GutterMarker { } } -interface GitBlameSourceViewerProps { +interface GitBlameRendererProps { filename: string source: string onViewUpdate?: (update: ViewUpdate) => void @@ -190,17 +186,22 @@ interface EditorLinePaddingWidgetSpec extends LineWidgetSpec { blockLines: number } -const GitBlameSourceViewer = React.memo(function GitBlameSourceViewer({ +const GitBlameRenderer = React.memo(function GitBlameSourceViewer({ source, filename, - onViewUpdate, + onViewUpdate = noop, blameBlocks -}: GitBlameSourceViewerProps) { - const view = useRef() - const ref = useRef() - const languageConfig = useMemo(() => new Compartment(), []) +}: GitBlameRendererProps) { + const extensions = useMemo(() => new Compartment(), []) + const viewRef = useRef() useEffect(() => { + const customLineNumberGutter = gutter({ + lineMarker(_view, line) { + const lineNumber: number = _view.state.doc.lineAt(line.from).number + return new CustomLineNumber(lineNumber) + } + }) const lineWidgetSpec: EditorLinePaddingWidgetSpec[] = [] Object.values(blameBlocks).forEach(block => { @@ -219,75 +220,30 @@ const GitBlameSourceViewer = React.memo(function GitBlameSourceViewer({ }) }) - const customLineNumberGutter = gutter({ - lineMarker(_view, line) { - const lineNumber: number = _view.state.doc.lineAt(line.from).number - return new CustomLineNumber(lineNumber) - } - }) - - const editorView = new EditorView({ - doc: source, - extensions: [ + viewRef.current?.dispatch({ + effects: extensions.reconfigure([ customLineNumberGutter, - - ViewPlugin.fromClass( - class { - update(update: ViewUpdate) { - onViewUpdate?.(update) - } - } - ), - - color, - hyperLink, // works pretty well in a markdown file - theme, - - EditorView.lineWrapping, - keymap.of([indentWithTab]), - - EditorState.readOnly.of(true), - EditorView.editable.of(false), - lineWidget({ spec: lineWidgetSpec, widgetFor: spec => new EditorLinePaddingWidget(spec) - }), - - /** - languageConfig is a compartment that defaults to an empty array (no language support) - at first, when a language is detected, languageConfig is used to reconfigure dynamically. - @see https://codemirror.net/examples/config/ - */ - languageConfig.of([]) - ], - parent: ref.current - }) - - view.current = editorView - - return () => { - editorView.destroy() - } - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (filename) { - languageDescriptionFrom(filename) - ?.load() - .then(languageSupport => { - view.current?.dispatch({ effects: languageConfig.reconfigure(languageSupport) }) }) - } - }, [filename, view, languageConfig]) + ]) + }) + }, [extensions, blameBlocks]) - return + return ( + + ) }) -function languageDescriptionFrom(filename: string) { - return LanguageDescription.matchFilename(languages, filename) -} - class EditorLinePaddingWidget extends WidgetType { constructor(readonly spec: EditorLinePaddingWidgetSpec) { super() @@ -306,9 +262,7 @@ class EditorLinePaddingWidget extends WidgetType { div.setAttribute('aria-hidden', 'true') div.setAttribute('data-line-number', String(lineNumber)) div.setAttribute('data-position', position) - div.style.height = `${height}px` - // div.style.backgroundColor = position === LineWidgetPosition.TOP ? 'cyan' : 'yellow' return div }