mirror of
https://github.com/harness/drone.git
synced 2025-05-10 12:20:40 +08:00
Merge branch 'ui/editor-replacement' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#24)
This commit is contained in:
commit
abd3110a5b
91
web/src/components/Editor/Editor.tsx
Normal file
91
web/src/components/Editor/Editor.tsx
Normal file
@ -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<EditorView | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||||
|
source,
|
||||||
|
filename,
|
||||||
|
onViewUpdate = noop,
|
||||||
|
readonly = false,
|
||||||
|
className,
|
||||||
|
extensions = new Compartment().of([]),
|
||||||
|
viewRef
|
||||||
|
}: EditorProps) {
|
||||||
|
const view = useRef<EditorView>()
|
||||||
|
const ref = useRef<HTMLDivElement>()
|
||||||
|
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 <Container ref={ref} className={className} />
|
||||||
|
})
|
||||||
|
|
||||||
|
function languageDescriptionFrom(filename: string) {
|
||||||
|
return LanguageDescription.matchFilename(languages, filename)
|
||||||
|
}
|
@ -1,21 +1,17 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Avatar, Container, FontVariation, Layout, StringSubstitute, Text } from '@harness/uicore'
|
import { Avatar, Container, FontVariation, Layout, StringSubstitute, Text } from '@harness/uicore'
|
||||||
import { LanguageDescription } from '@codemirror/language'
|
import type { ViewUpdate } from '@codemirror/view'
|
||||||
import { indentWithTab } from '@codemirror/commands'
|
import { EditorView, gutter, GutterMarker, WidgetType } from '@codemirror/view'
|
||||||
import { ViewPlugin, ViewUpdate } from '@codemirror/view'
|
import { Compartment } from '@codemirror/state'
|
||||||
import { languages } from '@codemirror/language-data'
|
|
||||||
import { EditorView, gutter, GutterMarker, keymap, WidgetType } from '@codemirror/view'
|
|
||||||
import { Compartment, EditorState } from '@codemirror/state'
|
|
||||||
import ReactTimeago from 'react-timeago'
|
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 { useGet } from 'restful-react'
|
||||||
import { Render } from 'react-jsx-match'
|
import { Render } from 'react-jsx-match'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
import type { GitrpcBlamePart } from 'services/code'
|
import type { GitrpcBlamePart } from 'services/code'
|
||||||
import type { GitInfoProps } from 'utils/GitUtils'
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import { Editor } from 'components/Editor/Editor'
|
||||||
import { lineWidget, LineWidgetPosition, LineWidgetSpec } from './lineWidget'
|
import { lineWidget, LineWidgetPosition, LineWidgetSpec } from './lineWidget'
|
||||||
import css from './GitBlame.module.scss'
|
import css from './GitBlame.module.scss'
|
||||||
|
|
||||||
@ -151,7 +147,7 @@ export const GitBlame: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'resourcePat
|
|||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
<Render when={Object.values(blameBlocks).length}>
|
<Render when={Object.values(blameBlocks).length}>
|
||||||
<GitBlameSourceViewer
|
<GitBlameRenderer
|
||||||
source={data?.map(({ lines }) => (lines as string[]).join('\n')).join('\n') || ''}
|
source={data?.map(({ lines }) => (lines as string[]).join('\n')).join('\n') || ''}
|
||||||
filename={resourcePath}
|
filename={resourcePath}
|
||||||
onViewUpdate={onViewUpdate}
|
onViewUpdate={onViewUpdate}
|
||||||
@ -179,7 +175,7 @@ class CustomLineNumber extends GutterMarker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitBlameSourceViewerProps {
|
interface GitBlameRendererProps {
|
||||||
filename: string
|
filename: string
|
||||||
source: string
|
source: string
|
||||||
onViewUpdate?: (update: ViewUpdate) => void
|
onViewUpdate?: (update: ViewUpdate) => void
|
||||||
@ -190,17 +186,22 @@ interface EditorLinePaddingWidgetSpec extends LineWidgetSpec {
|
|||||||
blockLines: number
|
blockLines: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const GitBlameSourceViewer = React.memo(function GitBlameSourceViewer({
|
const GitBlameRenderer = React.memo(function GitBlameSourceViewer({
|
||||||
source,
|
source,
|
||||||
filename,
|
filename,
|
||||||
onViewUpdate,
|
onViewUpdate = noop,
|
||||||
blameBlocks
|
blameBlocks
|
||||||
}: GitBlameSourceViewerProps) {
|
}: GitBlameRendererProps) {
|
||||||
const view = useRef<EditorView>()
|
const extensions = useMemo(() => new Compartment(), [])
|
||||||
const ref = useRef<HTMLDivElement>()
|
const viewRef = useRef<EditorView>()
|
||||||
const languageConfig = useMemo(() => new Compartment(), [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const customLineNumberGutter = gutter({
|
||||||
|
lineMarker(_view, line) {
|
||||||
|
const lineNumber: number = _view.state.doc.lineAt(line.from).number
|
||||||
|
return new CustomLineNumber(lineNumber)
|
||||||
|
}
|
||||||
|
})
|
||||||
const lineWidgetSpec: EditorLinePaddingWidgetSpec[] = []
|
const lineWidgetSpec: EditorLinePaddingWidgetSpec[] = []
|
||||||
|
|
||||||
Object.values(blameBlocks).forEach(block => {
|
Object.values(blameBlocks).forEach(block => {
|
||||||
@ -219,75 +220,30 @@ const GitBlameSourceViewer = React.memo(function GitBlameSourceViewer({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const customLineNumberGutter = gutter({
|
viewRef.current?.dispatch({
|
||||||
lineMarker(_view, line) {
|
effects: extensions.reconfigure([
|
||||||
const lineNumber: number = _view.state.doc.lineAt(line.from).number
|
|
||||||
return new CustomLineNumber(lineNumber)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const editorView = new EditorView({
|
|
||||||
doc: source,
|
|
||||||
extensions: [
|
|
||||||
customLineNumberGutter,
|
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({
|
lineWidget({
|
||||||
spec: lineWidgetSpec,
|
spec: lineWidgetSpec,
|
||||||
widgetFor: spec => new EditorLinePaddingWidget(spec)
|
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) })
|
|
||||||
})
|
})
|
||||||
}
|
}, [extensions, blameBlocks])
|
||||||
}, [filename, view, languageConfig])
|
|
||||||
|
|
||||||
return <Container ref={ref} className={css.main} />
|
return (
|
||||||
|
<Editor
|
||||||
|
viewRef={viewRef}
|
||||||
|
filename={filename}
|
||||||
|
source={source}
|
||||||
|
readonly={true}
|
||||||
|
className={css.main}
|
||||||
|
onViewUpdate={onViewUpdate}
|
||||||
|
extensions={extensions.of([])}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function languageDescriptionFrom(filename: string) {
|
|
||||||
return LanguageDescription.matchFilename(languages, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditorLinePaddingWidget extends WidgetType {
|
class EditorLinePaddingWidget extends WidgetType {
|
||||||
constructor(readonly spec: EditorLinePaddingWidgetSpec) {
|
constructor(readonly spec: EditorLinePaddingWidgetSpec) {
|
||||||
super()
|
super()
|
||||||
@ -306,9 +262,7 @@ class EditorLinePaddingWidget extends WidgetType {
|
|||||||
div.setAttribute('aria-hidden', 'true')
|
div.setAttribute('aria-hidden', 'true')
|
||||||
div.setAttribute('data-line-number', String(lineNumber))
|
div.setAttribute('data-line-number', String(lineNumber))
|
||||||
div.setAttribute('data-position', position)
|
div.setAttribute('data-position', position)
|
||||||
|
|
||||||
div.style.height = `${height}px`
|
div.style.height = `${height}px`
|
||||||
// div.style.backgroundColor = position === LineWidgetPosition.TOP ? 'cyan' : 'yellow'
|
|
||||||
|
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user