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 {
6+ bracketMatching ,
7+ syntaxHighlighting ,
8+ defaultHighlightStyle
9+ } from '@codemirror/language' ;
10+ import { closeBrackets , closeBracketsKeymap } from '@codemirror/autocomplete' ;
11+ import { defaultKeymap , history , historyKeymap } from '@codemirror/commands' ;
12+ import { javascript } from '@codemirror/lang-javascript' ;
13+
414import { useDispatch } from 'react-redux' ;
515import { Encode } from 'console-feed' ;
616
@@ -11,31 +21,24 @@ import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher';
1121// heavily inspired by
1222// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx
1323
24+ // TODO(connie): Add theme support?
1425function ConsoleInput ( { theme, fontSize } ) {
15- const [ commandHistory , setCommandHistory ] = useState ( [ ] ) ;
16- const [ commandCursor , setCommandCursor ] = useState ( - 1 ) ;
26+ const commandHistory = useRef ( [ ] ) ;
27+ const commandCursor = useRef ( - 1 ) ;
1728 const codemirrorContainer = useRef ( null ) ;
18- const cmInstance = useRef ( null ) ;
29+ const cmView = useRef ( null ) ;
1930 const dispatch = useDispatch ( ) ;
2031
2132 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 ;
33+ const enterKeymap = {
34+ key : 'Enter' ,
35+ shiftKey : ( ) => false , // Treat like a normal Enter key press if the Shift key is held down.
36+ preventDefault : true ,
37+ stopPropogation : true ,
38+ run : ( view ) => {
39+ const value = view . state . doc . toString ( ) . trim ( ) ;
40+ if ( value === '' || view . state . selection . main . empty === false )
41+ return false ;
3942
4043 const messages = [
4144 { log : Encode ( { method : 'command' , data : [ value ] } ) }
@@ -48,77 +51,91 @@ function ConsoleInput({ theme, fontSize }) {
4851 } ) ;
4952
5053 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 ) ;
54+ view . dispatch ( {
55+ changes : { from : 0 , to : view . state . doc . length , insert : '' }
56+ } ) ;
57+ commandHistory . current . unshift ( value ) ;
58+ commandCursor . current = - 1 ;
59+ return true ;
6460 }
6561 } ;
66- } , [ commandHistory ] ) ;
6762
68- useEffect ( ( ) => {
69- const handleUpArrowKey = ( cm , e ) => {
70- if ( e . key === 'ArrowUp' ) {
71- const lineNumber = cm . getDoc ( ) . getCursor ( ) . line ;
72- if ( lineNumber !== 0 ) return ;
63+ const upArrowKeymap = {
64+ key : 'ArrowUp' ,
65+ run : ( view ) => {
66+ // Just let the cursor go up if we have a multiline input
67+ // and the cursor isn't at the first line.
68+ const currentLine = view . state . doc . lineAt (
69+ view . state . selection . main . head
70+ ) . number ;
71+ // CM lines are 1-indexed, so the first line is 1.
72+ if ( currentLine > 1 ) return false ;
7373
7474 const newCursor = Math . min (
75- commandCursor + 1 ,
76- commandHistory . length - 1
75+ commandCursor . current + 1 ,
76+ commandHistory . current . length - 1
7777 ) ;
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 ) ;
78+ const newValue = commandHistory . current [ newCursor ] || '' ;
79+ view . dispatch ( {
80+ changes : { from : 0 , to : view . state . doc . length , insert : newValue }
81+ } ) ;
82+ const newCursorPos = newValue . length ;
83+ view . dispatch ( {
84+ selection : { anchor : newCursorPos , head : newCursorPos }
85+ } ) ;
86+ commandCursor . current = newCursor ;
87+ return true ;
9288 }
9389 } ;
94- } , [ commandCursor , commandHistory ] ) ;
9590
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 ) ;
91+ const downArrowKeymap = {
92+ key : 'ArrowDown' ,
93+ run : ( view ) => {
94+ // Just let the cursor go down if we have a multiline input
95+ // and the cursor isn't at the last line.
96+ const currentLine = view . state . doc . lineAt (
97+ view . state . selection . main . head
98+ ) . number ;
99+ const docLength = view . state . doc . lines ;
100+ if ( currentLine !== docLength ) return false ;
101+
102+ const newCursor = Math . max ( commandCursor . current - 1 , - 1 ) ;
103+ const newValue = commandHistory . current [ newCursor ] || '' ;
104+ view . dispatch ( {
105+ changes : { from : 0 , to : view . state . doc . length , insert : newValue }
106+ } ) ;
107+ const newCursorPos = newValue . length ;
108+ view . dispatch ( {
109+ selection : { anchor : newCursorPos , head : newCursorPos }
110+ } ) ;
111+ commandCursor . current = newCursor ;
112+ return true ;
109113 }
110114 } ;
111115
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 ] ) ;
116+ const cmState = EditorState . create ( {
117+ extensions : [
118+ history ( ) ,
119+ highlightSpecialChars ( ) ,
120+ bracketMatching ( ) ,
121+ closeBrackets ( ) ,
122+ syntaxHighlighting ( defaultHighlightStyle ) ,
123+ javascript ( ) ,
124+ keymap . of ( [
125+ enterKeymap ,
126+ upArrowKeymap ,
127+ downArrowKeymap ,
128+ ...defaultKeymap ,
129+ ...closeBracketsKeymap ,
130+ ...historyKeymap
131+ ] )
132+ ]
133+ } ) ;
134+ cmView . current = new EditorView ( {
135+ state : cmState ,
136+ parent : codemirrorContainer . current
137+ } ) ;
138+ } , [ ] ) ;
122139
123140 return (
124141 < div className = "console__input" >
0 commit comments