1
1
import 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
+
4
14
import { useDispatch } from 'react-redux' ;
5
15
import { Encode } from 'console-feed' ;
6
16
@@ -11,31 +21,24 @@ import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher';
11
21
// heavily inspired by
12
22
// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx
13
23
24
+ // TODO(connie): Add theme support?
14
25
function ConsoleInput ( { theme, fontSize } ) {
15
- const [ commandHistory , setCommandHistory ] = useState ( [ ] ) ;
16
- const [ commandCursor , setCommandCursor ] = useState ( - 1 ) ;
26
+ const commandHistory = useRef ( [ ] ) ;
27
+ const commandCursor = useRef ( - 1 ) ;
17
28
const codemirrorContainer = useRef ( null ) ;
18
- const cmInstance = useRef ( null ) ;
29
+ const cmView = useRef ( null ) ;
19
30
const dispatch = useDispatch ( ) ;
20
31
21
32
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 ;
39
42
40
43
const messages = [
41
44
{ log : Encode ( { method : 'command' , data : [ value ] } ) }
@@ -48,77 +51,91 @@ function ConsoleInput({ theme, fontSize }) {
48
51
} ) ;
49
52
50
53
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 ;
64
60
}
65
61
} ;
66
- } , [ commandHistory ] ) ;
67
62
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 ;
73
73
74
74
const newCursor = Math . min (
75
- commandCursor + 1 ,
76
- commandHistory . length - 1
75
+ commandCursor . current + 1 ,
76
+ commandHistory . current . length - 1
77
77
) ;
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 ;
92
88
}
93
89
} ;
94
- } , [ commandCursor , commandHistory ] ) ;
95
90
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 ;
109
113
}
110
114
} ;
111
115
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
+ } , [ ] ) ;
122
139
123
140
return (
124
141
< div className = "console__input" >
0 commit comments