11import PropTypes from 'prop-types' ;
2- import React , { useRef , useEffect , useState } from 'react' ;
3- import CodeMirror from 'codemirror' ;
2+ import React , { useRef , useEffect } from 'react' ;
3+ import { EditorState } from '@codemirror/state' ;
4+ import { EditorView , highlightSpecialChars , keymap } from '@codemirror/view' ;
5+ import { bracketMatching , syntaxHighlighting } from '@codemirror/language' ;
6+ import { closeBrackets , closeBracketsKeymap } from '@codemirror/autocomplete' ;
7+ import { defaultKeymap , history , historyKeymap } from '@codemirror/commands' ;
8+ import { javascript } from '@codemirror/lang-javascript' ;
9+
410import { useDispatch } from 'react-redux' ;
511import { Encode } from 'console-feed' ;
612
713import RightArrowIcon from '../../../images/right-arrow.svg' ;
814import { dispatchConsoleEvent } from '../actions/console' ;
915import { dispatchMessage , MessageTypes } from '../../../utils/dispatcher' ;
16+ import { highlightStyle } from './Editor/utils/highlightStyle' ;
1017
1118// heavily inspired by
1219// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx
1320
21+ // TODO(connie): Add theme support?
1422function ConsoleInput ( { theme, fontSize } ) {
15- const [ commandHistory , setCommandHistory ] = useState ( [ ] ) ;
16- const [ commandCursor , setCommandCursor ] = useState ( - 1 ) ;
23+ const commandHistory = useRef ( [ ] ) ;
24+ const commandCursor = useRef ( - 1 ) ;
1725 const codemirrorContainer = useRef ( null ) ;
18- const cmInstance = useRef ( null ) ;
26+ const cmView = useRef ( null ) ;
1927 const dispatch = useDispatch ( ) ;
2028
2129 useEffect ( ( ) => {
22- cmInstance . current = CodeMirror ( codemirrorContainer . current , {
23- theme : `p5-${ theme } ` ,
24- scrollbarStyle : null ,
25- keymap : 'sublime' ,
26- mode : 'javascript' ,
27- inputStyle : 'contenteditable'
28- } ) ;
29- } , [ ] ) ;
30-
31- useEffect ( ( ) => {
32- const handleEnterKey = ( cm , e ) => {
33- if ( e . key === 'Enter' && ! e . shiftKey ) {
34- e . preventDefault ( ) ;
35- e . stopPropagation ( ) ;
36-
37- const value = cm . getValue ( ) . trim ( ) ;
38- if ( value === '' ) return ;
30+ const enterKeymap = {
31+ key : 'Enter' ,
32+ shiftKey : ( ) => false , // Treat like a normal Enter key press if the Shift key is held down.
33+ preventDefault : true ,
34+ stopPropogation : true ,
35+ run : ( view ) => {
36+ const value = view . state . doc . toString ( ) . trim ( ) ;
37+ if ( value === '' || view . state . selection . main . empty === false )
38+ return false ;
3939
4040 const messages = [
4141 { log : Encode ( { method : 'command' , data : [ value ] } ) }
@@ -48,77 +48,91 @@ function ConsoleInput({ theme, fontSize }) {
4848 } ) ;
4949
5050 dispatch ( dispatchConsoleEvent ( consoleEvent ) ) ;
51- cm . setValue ( '' ) ;
52- setCommandHistory ( [ value , ...commandHistory ] ) ;
53- setCommandCursor ( - 1 ) ;
54- }
55- } ;
56-
57- if ( cmInstance . current ) {
58- cmInstance . current . on ( 'keydown' , handleEnterKey ) ;
59- }
60-
61- return ( ) => {
62- if ( cmInstance . current ) {
63- cmInstance . current . off ( 'keydown' , handleEnterKey ) ;
51+ view . dispatch ( {
52+ changes : { from : 0 , to : view . state . doc . length , insert : '' }
53+ } ) ;
54+ commandHistory . current . unshift ( value ) ;
55+ commandCursor . current = - 1 ;
56+ return true ;
6457 }
6558 } ;
66- } , [ commandHistory ] ) ;
6759
68- useEffect ( ( ) => {
69- const handleUpArrowKey = ( cm , e ) => {
70- if ( e . key === 'ArrowUp' ) {
71- const lineNumber = cm . getDoc ( ) . getCursor ( ) . line ;
72- if ( lineNumber !== 0 ) return ;
60+ const upArrowKeymap = {
61+ key : 'ArrowUp' ,
62+ run : ( view ) => {
63+ // Just let the cursor go up if we have a multiline input
64+ // and the cursor isn't at the first line.
65+ const currentLine = view . state . doc . lineAt (
66+ view . state . selection . main . head
67+ ) . number ;
68+ // CM lines are 1-indexed, so the first line is 1.
69+ if ( currentLine > 1 ) return false ;
7370
7471 const newCursor = Math . min (
75- commandCursor + 1 ,
76- commandHistory . length - 1
72+ commandCursor . current + 1 ,
73+ commandHistory . current . length - 1
7774 ) ;
78- cm . setValue ( commandHistory [ newCursor ] || '' ) ;
79- const cursorPos = cm . getDoc ( ) . getLine ( 0 ) . length - 1 ;
80- cm . getDoc ( ) . setCursor ( { line : 0 , ch : cursorPos } ) ;
81- setCommandCursor ( newCursor ) ;
82- }
83- } ;
84-
85- if ( cmInstance . current ) {
86- cmInstance . current . on ( 'keydown' , handleUpArrowKey ) ;
87- }
88-
89- return ( ) => {
90- if ( cmInstance . current ) {
91- cmInstance . current . off ( 'keydown' , handleUpArrowKey ) ;
75+ const newValue = commandHistory . current [ newCursor ] || '' ;
76+ view . dispatch ( {
77+ changes : { from : 0 , to : view . state . doc . length , insert : newValue }
78+ } ) ;
79+ const newCursorPos = newValue . length ;
80+ view . dispatch ( {
81+ selection : { anchor : newCursorPos , head : newCursorPos }
82+ } ) ;
83+ commandCursor . current = newCursor ;
84+ return true ;
9285 }
9386 } ;
94- } , [ commandCursor , commandHistory ] ) ;
9587
96- useEffect ( ( ) => {
97- const handleArrowDownKey = ( cm , e ) => {
98- if ( e . key === 'ArrowDown' ) {
99- const lineNumber = cm . getDoc ( ) . getCursor ( ) . line ;
100- const lineCount = cm . lineCount ( ) ;
101- if ( lineNumber + 1 !== lineCount ) return ;
102-
103- const newCursor = Math . max ( commandCursor - 1 , - 1 ) ;
104- cm . setValue ( commandHistory [ newCursor ] || '' ) ;
105- const newLine = cm . getDoc ( ) . getLine ( lineCount - 1 ) ;
106- const cursorPos = newLine ? newLine . length - 1 : 1 ;
107- cm . getDoc ( ) . setCursor ( { line : lineCount - 1 , ch : cursorPos } ) ;
108- setCommandCursor ( newCursor ) ;
88+ const downArrowKeymap = {
89+ key : 'ArrowDown' ,
90+ run : ( view ) => {
91+ // Just let the cursor go down if we have a multiline input
92+ // and the cursor isn't at the last line.
93+ const currentLine = view . state . doc . lineAt (
94+ view . state . selection . main . head
95+ ) . number ;
96+ const docLength = view . state . doc . lines ;
97+ if ( currentLine !== docLength ) return false ;
98+
99+ const newCursor = Math . max ( commandCursor . current - 1 , - 1 ) ;
100+ const newValue = commandHistory . current [ newCursor ] || '' ;
101+ view . dispatch ( {
102+ changes : { from : 0 , to : view . state . doc . length , insert : newValue }
103+ } ) ;
104+ const newCursorPos = newValue . length ;
105+ view . dispatch ( {
106+ selection : { anchor : newCursorPos , head : newCursorPos }
107+ } ) ;
108+ commandCursor . current = newCursor ;
109+ return true ;
109110 }
110111 } ;
111112
112- if ( cmInstance . current ) {
113- cmInstance . current . on ( 'keydown' , handleArrowDownKey ) ;
114- }
115-
116- return ( ) => {
117- if ( cmInstance . current ) {
118- cmInstance . current . off ( 'keydown' , handleArrowDownKey ) ;
119- }
120- } ;
121- } , [ commandCursor , commandHistory ] ) ;
113+ const cmState = EditorState . create ( {
114+ extensions : [
115+ history ( ) ,
116+ highlightSpecialChars ( ) ,
117+ bracketMatching ( ) ,
118+ closeBrackets ( ) ,
119+ syntaxHighlighting ( highlightStyle ) ,
120+ javascript ( ) ,
121+ keymap . of ( [
122+ enterKeymap ,
123+ upArrowKeymap ,
124+ downArrowKeymap ,
125+ ...defaultKeymap ,
126+ ...closeBracketsKeymap ,
127+ ...historyKeymap
128+ ] )
129+ ]
130+ } ) ;
131+ cmView . current = new EditorView ( {
132+ state : cmState ,
133+ parent : codemirrorContainer . current
134+ } ) ;
135+ } , [ ] ) ;
122136
123137 return (
124138 < div className = "console__input" >
0 commit comments