feat: moving markdown editing to monaco (#18231)
parent
dae7953806
commit
448b598834
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
|||
import loader from 'src/external/monaco.onigasm'
|
||||
import {Registry} from 'monaco-textmate' // peer dependency
|
||||
import {wireTmGrammars} from 'monaco-editor-textmate'
|
||||
|
||||
import {MonacoType} from 'src/types'
|
||||
|
||||
const LANGID = 'markdown'
|
||||
|
||||
async function addSyntax(monaco: MonacoType) {
|
||||
monaco.languages.register({id: LANGID})
|
||||
|
||||
await loader()
|
||||
|
||||
const registry = new Registry({
|
||||
getGrammarDefinition: async () => ({
|
||||
format: 'json',
|
||||
content: await import(/* webpackPrefetch: 0 */ 'src/external/markdown.tmLanguage.json').then(
|
||||
data => {
|
||||
return JSON.stringify(data)
|
||||
}
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
// map of monaco "language id's" to TextMate scopeNames
|
||||
const grammars = new Map()
|
||||
grammars.set(LANGID, LANGID)
|
||||
|
||||
await wireTmGrammars(monaco, registry, grammars)
|
||||
}
|
||||
|
||||
addSyntax(window.monaco)
|
||||
|
||||
export default LANGID
|
|
@ -0,0 +1,25 @@
|
|||
import {MonacoType} from 'src/types'
|
||||
|
||||
const THEME_NAME = 'markdownTheme'
|
||||
|
||||
function addTheme(monaco: MonacoType) {
|
||||
monaco.editor.defineTheme(THEME_NAME, {
|
||||
base: 'vs-dark',
|
||||
inherit: false,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.foreground': '#F8F8F8',
|
||||
'editor.background': '#202028',
|
||||
'editorGutter.background': '#25252e',
|
||||
'editor.selectionBackground': '#353640',
|
||||
'editorLineNumber.foreground': '#666978',
|
||||
'editor.lineHighlightBackground': '#353640',
|
||||
'editorCursor.foreground': '#ffffff',
|
||||
'editorActiveLineNumber.foreground': '#bec2cc',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
addTheme(window.monaco)
|
||||
|
||||
export default THEME_NAME
|
|
@ -7,7 +7,7 @@ import {MarkdownMode} from './'
|
|||
|
||||
// Components
|
||||
import MarkdownModeToggle from './MarkdownModeToggle'
|
||||
import MarkdownPanelEditor from './MarkdownPanelEditor'
|
||||
import MarkdownMonacoEditor from 'src/shared/components/MarkdownMonacoEditor'
|
||||
import {MarkdownRenderer} from 'src/shared/components/views/MarkdownRenderer'
|
||||
|
||||
const MarkdownPanel: FC<PipeProp> = ({data, Context, onUpdate}) => {
|
||||
|
@ -24,7 +24,11 @@ const MarkdownPanel: FC<PipeProp> = ({data, Context, onUpdate}) => {
|
|||
}
|
||||
|
||||
let panelContents = (
|
||||
<MarkdownPanelEditor text={data.text} onChange={handleChange} />
|
||||
<MarkdownMonacoEditor
|
||||
script={data.text}
|
||||
onChangeScript={handleChange}
|
||||
autogrow
|
||||
/>
|
||||
)
|
||||
|
||||
if (data.mode === 'preview') {
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
onChange: (text: string) => void
|
||||
}
|
||||
|
||||
const MarkdownPanelEditor: FC<Props> = ({text, onChange}) => {
|
||||
const handleTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
onChange(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className="notebook-panel--markdown-editor"
|
||||
value={text}
|
||||
onChange={handleTextAreaChange}
|
||||
autoFocus={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownPanelEditor
|
|
@ -4,12 +4,173 @@ import './style.scss'
|
|||
|
||||
export type MarkdownMode = 'edit' | 'preview'
|
||||
|
||||
const TEST_MODE = false
|
||||
const TESTCASE = ` An h1 header
|
||||
============
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. *Italic*, **bold**, and \`monospace\`. Itemized lists
|
||||
look like:
|
||||
|
||||
* this one
|
||||
* that one
|
||||
* the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
|
||||
> Block quotes are
|
||||
> written like so.
|
||||
>
|
||||
> They can span multiple paragraphs,
|
||||
> if you like.
|
||||
|
||||
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
|
||||
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
|
||||
Unicode is supported. ☺
|
||||
|
||||
|
||||
|
||||
An h2 header
|
||||
------------
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
1. first item
|
||||
2. second item
|
||||
3. third item
|
||||
|
||||
Note again how the actual text starts at 4 columns in (4 characters
|
||||
from the left side). Here's a code sample:
|
||||
|
||||
# Let me re-iterate ...
|
||||
for i in 1 .. 10 { do-something(i) }
|
||||
|
||||
As you probably guessed, indented 4 spaces. By the way, instead of
|
||||
indenting the block, you can use delimited blocks, if you like:
|
||||
|
||||
~~
|
||||
define foobar() {
|
||||
print "Welcome to flavor country!";
|
||||
}
|
||||
~~
|
||||
|
||||
(which makes copying & pasting easier). You can optionally mark the
|
||||
delimited block for Pandoc to syntax highlight it:
|
||||
|
||||
~~python
|
||||
import time
|
||||
# Quick, count to ten!
|
||||
for i in range(10):
|
||||
# (but not *too* quick)
|
||||
time.sleep(0.5)
|
||||
print(i)
|
||||
~~
|
||||
|
||||
|
||||
|
||||
### An h3 header ###
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
* carrots
|
||||
* celery
|
||||
* lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
|
||||
find wooden spoon
|
||||
uncover pot
|
||||
stir
|
||||
cover pot
|
||||
balance wooden spoon precariously on pot handle
|
||||
wait 10 minutes
|
||||
goto first step (or shut off burner when done)
|
||||
|
||||
Do not bump wooden spoon or it will fall.
|
||||
|
||||
Notice again how text always lines up on 4-space indents (including
|
||||
that last line which continues item 3 above).
|
||||
|
||||
Here's a link to [a website](http://foo.bar), to a [local
|
||||
doc](local-doc.html), and to a [section heading in the current
|
||||
doc](#an-h2-header). Here's a footnote [^1].
|
||||
|
||||
[^1]: Some footnote text.
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
Name Size Material Color
|
||||
------------- ----- ------------ ------------
|
||||
All Business 9 leather brown
|
||||
Roundabout 10 hemp canvas natural
|
||||
Cinderella 11 glass transparent
|
||||
|
||||
Table: Shoes sizes, materials, and colors.
|
||||
|
||||
(The above is the caption for the table.) Pandoc also supports
|
||||
multi-line tables:
|
||||
|
||||
-------- -----------------------
|
||||
Keyword Text
|
||||
-------- -----------------------
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
-------- -----------------------
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
***
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
apples
|
||||
: Good for making applesauce.
|
||||
|
||||
oranges
|
||||
: Citrus!
|
||||
|
||||
tomatoes
|
||||
: There's no "e" in tomatoe.
|
||||
|
||||
Again, text is indented 4 spaces. (Put a blank line between each
|
||||
term and its definition to spread things out more.)
|
||||
|
||||
Here's a "line block" (note how whitespace is honored):
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
|
||||
![example image](example-image.jpg "An exemplary image")
|
||||
|
||||
Inline math equation: $\omega = d\phi / dt$. Display
|
||||
math should get its own line like so:
|
||||
|
||||
$$I = \int \rho R^{2} dV$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.`
|
||||
|
||||
register({
|
||||
type: 'markdown',
|
||||
component: MarkdownPanel,
|
||||
button: 'Markdown',
|
||||
initial: () => ({
|
||||
text: 'Content',
|
||||
text: TEST_MODE ? TESTCASE : '',
|
||||
mode: 'edit',
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -21,25 +21,11 @@ $notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%);
|
|||
}
|
||||
}
|
||||
|
||||
.notebook-panel--markdown-editor {
|
||||
padding: $cf-marg-b;
|
||||
background-color: $g1-raven;
|
||||
border-color: $cf-input-border--default;
|
||||
color: $cf-input-text--default;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
transition: $cf-input--transition;
|
||||
outline: none;
|
||||
.notebook-panel--body .markdown-editor--monaco {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $cf-input-text--hover;
|
||||
border-color: $cf-input-border--hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: $cf-input-text--focused;
|
||||
border-color: $cf-input-border--focused;
|
||||
box-shadow: $cf-input--box-shadow;
|
||||
.react-monaco-editor-container {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,10 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
|
|||
border-radius: $cf-border / 2;
|
||||
margin: $cf-marg-a 0;
|
||||
}
|
||||
|
||||
&:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight panel on hover
|
||||
|
|
|
@ -22,15 +22,19 @@ import {editor as monacoEditor} from 'monaco-editor'
|
|||
import {Diagnostic} from 'monaco-languageclient/lib/services'
|
||||
|
||||
const p2m = new ProtocolToMonacoConverter()
|
||||
interface Props {
|
||||
|
||||
export interface EditorProps {
|
||||
script: string
|
||||
onChangeScript: OnChangeScript
|
||||
onSubmitScript?: () => void
|
||||
setEditorInstance?: (editor: EditorType) => void
|
||||
skipFocus?: boolean
|
||||
autogrow?: boolean
|
||||
}
|
||||
|
||||
interface Props extends EditorProps {
|
||||
setEditorInstance?: (editor: EditorType) => void
|
||||
}
|
||||
|
||||
const FluxEditorMonaco: FC<Props> = ({
|
||||
script,
|
||||
onChangeScript,
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import MonacoEditor from 'react-monaco-editor'
|
||||
|
||||
// Utils
|
||||
import LANGID from 'src/external/monaco.markdown.syntax'
|
||||
import THEME_NAME from 'src/external/monaco.markdown.theme'
|
||||
import {registerAutogrow} from 'src/external/monaco.autogrow'
|
||||
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
||||
|
||||
// Types
|
||||
import {EditorType} from 'src/types'
|
||||
|
||||
import './FluxMonacoEditor.scss'
|
||||
|
||||
import {EditorProps} from 'src/shared/components/FluxMonacoEditor'
|
||||
|
||||
const MarkdownMonacoEditor: FC<EditorProps> = ({
|
||||
script,
|
||||
onChangeScript,
|
||||
skipFocus,
|
||||
autogrow,
|
||||
}) => {
|
||||
const editorDidMount = (editor: EditorType) => {
|
||||
if (autogrow) {
|
||||
registerAutogrow(editor)
|
||||
}
|
||||
|
||||
if (isFlagEnabled('cursorAtEOF')) {
|
||||
if (!skipFocus) {
|
||||
const lines = (script || '').split('\n')
|
||||
editor.setPosition({
|
||||
lineNumber: lines.length,
|
||||
column: lines[lines.length - 1].length + 1,
|
||||
})
|
||||
editor.focus()
|
||||
}
|
||||
} else {
|
||||
editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = (text: string) => {
|
||||
onChangeScript(text)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markdown-editor--monaco" data-testid="markdown-editor">
|
||||
<MonacoEditor
|
||||
language={LANGID}
|
||||
theme={THEME_NAME}
|
||||
value={script}
|
||||
onChange={onChange}
|
||||
options={{
|
||||
fontSize: 13,
|
||||
fontFamily: '"IBMPlexMono", monospace',
|
||||
cursorWidth: 2,
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: 0,
|
||||
minimap: {
|
||||
renderCharacters: false,
|
||||
},
|
||||
contextmenu: false,
|
||||
overviewRulerBorder: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
editorDidMount={editorDidMount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownMonacoEditor
|
Loading…
Reference in New Issue