-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support dock re-positioning and fix some editor quirks (#97)
- Loading branch information
1 parent
4a99eec
commit b7b884a
Showing
27 changed files
with
1,477 additions
and
927 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import React, { useRef, useEffect } from 'react'; | ||
import 'codemirror/lib/codemirror.css'; | ||
import 'codemirror/theme/neo.css'; | ||
|
||
import { formatCode as format } from '../../utils/formatting'; | ||
|
||
import styles from './CodeEditor.less'; | ||
|
||
import { Controlled as ReactCodeMirror } from 'react-codemirror2'; | ||
import 'codemirror/mode/jsx/jsx'; | ||
import 'codemirror/addon/edit/closetag'; | ||
import 'codemirror/addon/edit/closebrackets'; | ||
import 'codemirror/addon/hint/show-hint'; | ||
import 'codemirror/addon/hint/xml-hint'; | ||
import compileJsx from '../../utils/compileJsx'; | ||
|
||
const completeAfter = (cm, predicate) => { | ||
const CodeMirror = cm.constructor; | ||
if (!predicate || predicate()) { | ||
setTimeout(() => { | ||
if (!cm.state.completionActive) { | ||
cm.showHint({ completeSingle: false }); | ||
} | ||
}, 100); | ||
} | ||
|
||
return CodeMirror.Pass; | ||
}; | ||
|
||
const completeIfAfterLt = cm => { | ||
const CodeMirror = cm.constructor; | ||
|
||
return completeAfter(cm, () => { | ||
const cur = cm.getCursor(); | ||
// eslint-disable-next-line new-cap | ||
return cm.getRange(CodeMirror.Pos(cur.line, cur.ch - 1), cur) === '<'; | ||
}); | ||
}; | ||
|
||
const completeIfInTag = cm => { | ||
const CodeMirror = cm.constructor; | ||
|
||
return completeAfter(cm, () => { | ||
const tok = cm.getTokenAt(cm.getCursor()); | ||
if ( | ||
tok.type === 'string' && | ||
(!/['"]/.test(tok.string.charAt(tok.string.length - 1)) || | ||
tok.string.length === 1) | ||
) { | ||
return false; | ||
} | ||
const inner = CodeMirror.innerMode(cm.getMode(), tok.state).state; | ||
return inner.tagName; | ||
}); | ||
}; | ||
|
||
const validateCode = (editorInstance, code) => { | ||
editorInstance.clearGutter(styles.gutter); | ||
|
||
try { | ||
compileJsx(code); | ||
} catch (err) { | ||
const errorMessage = err && (err.message || ''); | ||
const matches = errorMessage.match(/\(([0-9]+):/); | ||
const lineNumber = | ||
matches && matches.length >= 2 && matches[1] && parseInt(matches[1], 10); | ||
|
||
if (lineNumber) { | ||
const marker = document.createElement('div'); | ||
marker.classList.add(styles.marker); | ||
marker.setAttribute('title', err.message); | ||
editorInstance.setGutterMarker(lineNumber - 1, styles.gutter, marker); | ||
} | ||
} | ||
}; | ||
|
||
export const CodeEditor = ({ code, onChange, hints }) => { | ||
const editorInstanceRef = useRef(null); | ||
|
||
useEffect( | ||
() => { | ||
const handleKeyDown = e => { | ||
if ( | ||
editorInstanceRef && | ||
editorInstanceRef.current && | ||
e.keyCode === 83 && | ||
(navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) | ||
) { | ||
e.preventDefault(); | ||
|
||
const { formattedCode, line, ch } = format({ | ||
code, | ||
cursor: editorInstanceRef.current.getCursor() | ||
}); | ||
|
||
onChange(formattedCode); | ||
|
||
setTimeout(() => { | ||
editorInstanceRef.current.focus(); | ||
editorInstanceRef.current.setCursor({ | ||
line, | ||
ch | ||
}); | ||
}); | ||
} | ||
}; | ||
|
||
window.addEventListener('keydown', handleKeyDown); | ||
|
||
return () => { | ||
window.removeEventListener('keydown', handleKeyDown); | ||
}; | ||
}, | ||
[code, onChange] | ||
); | ||
|
||
return ( | ||
<ReactCodeMirror | ||
editorDidMount={editorInstance => { | ||
editorInstanceRef.current = editorInstance; | ||
validateCode(editorInstance, code); | ||
editorInstance.focus(); | ||
editorInstance.setCursor(0, 0); | ||
}} | ||
value={code} | ||
onBeforeChange={(editor, data, newCode) => { | ||
onChange(newCode); | ||
validateCode(editorInstanceRef.current, newCode); | ||
}} | ||
options={{ | ||
mode: 'jsx', | ||
autoCloseTags: true, | ||
autoCloseBrackets: true, | ||
theme: 'neo', | ||
gutters: [styles.gutter], | ||
hintOptions: { schemaInfo: hints }, | ||
viewportMargin: 50, | ||
extraKeys: { | ||
Tab: cm => { | ||
if (cm.somethingSelected()) { | ||
cm.indentSelection('add'); | ||
} else { | ||
const indent = cm.getOption('indentUnit'); | ||
const spaces = Array(indent + 1).join(' '); | ||
cm.replaceSelection(spaces); | ||
} | ||
}, | ||
"'<'": completeAfter, | ||
"'/'": completeIfAfterLt, | ||
"' '": completeIfInTag, | ||
"'='": completeIfInTag | ||
} | ||
}} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
@import (reference) '../variables.less'; | ||
|
||
@editor-font-family: Source Code Pro, Firacode, Hasklig, Menlo, monospace; | ||
@sandbox-editor-height: 30vh; | ||
@gutter-size: 40px; | ||
|
||
.gutter { | ||
@box-fade-size: 10px; | ||
width: @gutter-size - @box-fade-size; | ||
background: white; | ||
position: absolute; | ||
right: @box-fade-size; | ||
box-shadow: 0 0 @box-fade-size 5px white; | ||
} | ||
|
||
.marker { | ||
width: @sandbox-marker-size; | ||
height: @sandbox-marker-size; | ||
border-radius: (@sandbox-marker-size / 2); | ||
background-color: @sandbox-marker-color; | ||
position: relative; | ||
top: (@sandbox-marker-size / 2); | ||
left: 10px; | ||
} | ||
|
||
:global { | ||
.react-codemirror2 { | ||
height: 100%; | ||
background-color: #fff; | ||
} | ||
|
||
.CodeMirror { | ||
height: 100%; | ||
width: 100%; | ||
font-family: @editor-font-family; | ||
} | ||
|
||
.CodeMirror-gutters { | ||
width: @gutter-size; | ||
} | ||
|
||
.CodeMirror-gutter-elt { | ||
position: relative; | ||
} | ||
|
||
.CodeMirror pre, | ||
.CodeMirror-linenumber { | ||
font-size: 16px; | ||
} | ||
|
||
.CodeMirror-line > span[role='presentation'] { | ||
padding-right: (@dock-size + @dock-fade-distance + @gutter-size) !important; | ||
} | ||
|
||
.CodeMirror-lines { | ||
padding: 16px 0; | ||
} | ||
|
||
.CodeMirror-hints { | ||
position: absolute; | ||
z-index: 10; | ||
overflow: hidden; | ||
list-style: none; | ||
margin: 0; | ||
padding: 0; | ||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); | ||
border-radius: 3px; | ||
background: white; | ||
font-size: 90%; | ||
line-height: 150%; | ||
font-family: @editor-font-family; | ||
max-height: 20em; | ||
overflow-y: auto; | ||
} | ||
|
||
.CodeMirror-hint { | ||
margin: 0; | ||
padding: 4px 8px; | ||
border-radius: 2px; | ||
white-space: pre; | ||
color: black; | ||
cursor: pointer; | ||
} | ||
|
||
li.CodeMirror-hint-active { | ||
background: #08f; | ||
color: white; | ||
} | ||
|
||
.cm-s-neo { | ||
&.CodeMirror { | ||
background-color: #fff; | ||
} | ||
.CodeMirror-cursor { | ||
background: black; | ||
width: 2px; | ||
} | ||
.CodeMirror-gutters { | ||
background: none; | ||
border: none; | ||
pointer-events: none; | ||
} | ||
.CodeMirror-linenumber { | ||
color: white; | ||
} | ||
.cm-tag { | ||
color: #040080; | ||
} | ||
.cm-attribute { | ||
color: #005ad2; | ||
} | ||
.cm-string { | ||
color: #00439c; | ||
} | ||
.cm-atom { | ||
color: #00439c; | ||
} | ||
.cm-variable { | ||
color: #827eff; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react'; | ||
import EditorUndockedSvg from './icons/EditorUndockedSvg'; | ||
import EditorLeftSvg from './icons/EditorLeftSvg'; | ||
import EditorBottomSvg from './icons/EditorBottomSvg'; | ||
import EditorRightSvg from './icons/EditorRightSvg'; | ||
|
||
import styles from './DockPosition.less'; | ||
|
||
export default ({ position, setPosition }) => { | ||
return ( | ||
<div className={styles.root}> | ||
<div className={styles.container}> | ||
<div className={styles.currentPosition}> | ||
{ | ||
{ | ||
undocked: <EditorUndockedSvg />, | ||
left: <EditorLeftSvg />, | ||
right: <EditorRightSvg />, | ||
bottom: <EditorBottomSvg /> | ||
}[position] | ||
} | ||
</div> | ||
<div className={styles.buttons}> | ||
{position !== 'undocked' && ( | ||
<button | ||
title="Undock editor" | ||
className={styles.button} | ||
onClick={() => setPosition('undocked')} | ||
> | ||
<EditorUndockedSvg /> | ||
</button> | ||
)} | ||
{position !== 'left' && ( | ||
<button | ||
title="Dock editor to the left" | ||
className={styles.button} | ||
onClick={() => setPosition('left')} | ||
> | ||
<EditorLeftSvg /> | ||
</button> | ||
)} | ||
{position !== 'right' && ( | ||
<button | ||
title="Dock editor to the right" | ||
className={styles.button} | ||
onClick={() => setPosition('right')} | ||
> | ||
<EditorRightSvg /> | ||
</button> | ||
)} | ||
{position !== 'bottom' && ( | ||
<button | ||
title="Dock editor to the bottom" | ||
className={styles.button} | ||
onClick={() => setPosition('bottom')} | ||
> | ||
<EditorBottomSvg /> | ||
</button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.