Skip to content

Commit 9a3cd35

Browse files
authored
Merge branch 'develop' into refactor/nav
2 parents 37fcaf1 + a51f765 commit 9a3cd35

24 files changed

+1534
-26
lines changed

client/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const SET_TEXT_OUTPUT = 'SET_TEXT_OUTPUT';
7575
export const SET_GRID_OUTPUT = 'SET_GRID_OUTPUT';
7676
export const SET_SOUND_OUTPUT = 'SET_SOUND_OUTPUT';
7777
export const SET_AUTOCLOSE_BRACKETS_QUOTES = 'SET_AUTOCLOSE_BRACKETS_QUOTES';
78+
export const SET_AUTOCOMPLETE_HINTER = 'SET_AUTOCOMPLETE_HINTER';
7879

7980
export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS';
8081
export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS';

client/modules/IDE/actions/preferences.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ function updatePreferences(formParams, dispatch) {
1616
}
1717

1818
export function setFontSize(value) {
19-
return (dispatch, getState) => { // eslint-disable-line
19+
return (dispatch, getState) => {
20+
// eslint-disable-line
2021
dispatch({
2122
type: ActionTypes.SET_FONT_SIZE,
2223
value
@@ -69,6 +70,24 @@ export function setAutocloseBracketsQuotes(value) {
6970
};
7071
}
7172

73+
export function setAutocompleteHinter(value) {
74+
return (dispatch, getState) => {
75+
dispatch({
76+
type: ActionTypes.SET_AUTOCOMPLETE_HINTER,
77+
value
78+
});
79+
const state = getState();
80+
if (state.user.authenticated) {
81+
const formParams = {
82+
preferences: {
83+
autocompleteHinter: value
84+
}
85+
};
86+
updatePreferences(formParams, dispatch);
87+
}
88+
};
89+
}
90+
7291
export function setAutosave(value) {
7392
return (dispatch, getState) => {
7493
dispatch({

client/modules/IDE/components/Editor.jsx

+116-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
33
import CodeMirror from 'codemirror';
4+
import Fuse from 'fuse.js';
45
import emmet from '@emmetio/codemirror-plugin';
56
import prettier from 'prettier/standalone';
67
import babelParser from 'prettier/parser-babel';
@@ -29,6 +30,7 @@ import 'codemirror/addon/search/jump-to-line';
2930
import 'codemirror/addon/edit/matchbrackets';
3031
import 'codemirror/addon/edit/closebrackets';
3132
import 'codemirror/addon/selection/mark-selection';
33+
import 'codemirror/addon/hint/css-hint';
3234
import 'codemirror-colorpicker';
3335

3436
import { JSHINT } from 'jshint';
@@ -43,6 +45,8 @@ import '../../../utils/p5-javascript';
4345
import Timer from '../components/Timer';
4446
import EditorAccessibility from '../components/EditorAccessibility';
4547
import { metaKey } from '../../../utils/metaKey';
48+
import './show-hint';
49+
import * as hinter from '../../../utils/p5-hinter';
4650

4751
import '../../../utils/codemirror-search';
4852

@@ -94,7 +98,6 @@ class Editor extends React.Component {
9498
this.beep = new Audio(beepUrl);
9599
this.widgets = [];
96100
this._cm = CodeMirror(this.codemirrorContainer, {
97-
// eslint-disable-line
98101
theme: `p5-${this.props.theme}`,
99102
lineNumbers: this.props.lineNumbers,
100103
styleActiveLine: true,
@@ -131,6 +134,11 @@ class Editor extends React.Component {
131134
}
132135
});
133136

137+
this.hinter = new Fuse(hinter.p5Hinter, {
138+
threshold: 0.05,
139+
keys: ['text']
140+
});
141+
134142
delete this._cm.options.lint.options.errors;
135143

136144
const replaceCommand =
@@ -186,16 +194,21 @@ class Editor extends React.Component {
186194
});
187195

188196
this._cm.on('keydown', (_cm, e) => {
189-
// 70 === f
190197
if (
191198
((metaKey === 'Cmd' && e.metaKey) ||
192199
(metaKey === 'Ctrl' && e.ctrlKey)) &&
193200
e.shiftKey &&
194-
e.keyCode === 70
201+
e.key === 'f'
195202
) {
196203
e.preventDefault();
197204
this.tidyCode();
198205
}
206+
207+
// Show hint
208+
const mode = this._cm.getOption('mode');
209+
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) {
210+
this.showHint(_cm);
211+
}
199212
});
200213

201214
this._cm.getWrapperElement().style[
@@ -253,6 +266,12 @@ class Editor extends React.Component {
253266
this.props.autocloseBracketsQuotes
254267
);
255268
}
269+
if (this.props.autocompleteHinter !== prevProps.autocompleteHinter) {
270+
if (!this.props.autocompleteHinter) {
271+
// close the hinter window once the preference is turned off
272+
CodeMirror.showHint(this._cm, () => {}, {});
273+
}
274+
}
256275

257276
if (this.props.runtimeErrorWarningVisible) {
258277
if (this.props.consoleEvents.length !== prevProps.consoleEvents.length) {
@@ -331,6 +350,99 @@ class Editor extends React.Component {
331350
this._cm.execCommand('findPersistent');
332351
}
333352

353+
showHint(_cm) {
354+
if (!this.props.autocompleteHinter) {
355+
CodeMirror.showHint(_cm, () => {}, {});
356+
return;
357+
}
358+
359+
let focusedLinkElement = null;
360+
const setFocusedLinkElement = (set) => {
361+
if (set && !focusedLinkElement) {
362+
const activeItemLink = document.querySelector(
363+
`.CodeMirror-hint-active a`
364+
);
365+
if (activeItemLink) {
366+
focusedLinkElement = activeItemLink;
367+
focusedLinkElement.classList.add('focused-hint-link');
368+
focusedLinkElement.parentElement.parentElement.classList.add(
369+
'unfocused'
370+
);
371+
}
372+
}
373+
};
374+
const removeFocusedLinkElement = () => {
375+
if (focusedLinkElement) {
376+
focusedLinkElement.classList.remove('focused-hint-link');
377+
focusedLinkElement.parentElement.parentElement.classList.remove(
378+
'unfocused'
379+
);
380+
focusedLinkElement = null;
381+
return true;
382+
}
383+
return false;
384+
};
385+
386+
const hintOptions = {
387+
_fontSize: this.props.fontSize,
388+
completeSingle: false,
389+
extraKeys: {
390+
'Shift-Right': (cm, e) => {
391+
const activeItemLink = document.querySelector(
392+
`.CodeMirror-hint-active a`
393+
);
394+
if (activeItemLink) activeItemLink.click();
395+
},
396+
Right: (cm, e) => {
397+
setFocusedLinkElement(true);
398+
},
399+
Left: (cm, e) => {
400+
removeFocusedLinkElement();
401+
},
402+
Up: (cm, e) => {
403+
const onLink = removeFocusedLinkElement();
404+
e.moveFocus(-1);
405+
setFocusedLinkElement(onLink);
406+
},
407+
Down: (cm, e) => {
408+
const onLink = removeFocusedLinkElement();
409+
e.moveFocus(1);
410+
setFocusedLinkElement(onLink);
411+
},
412+
Enter: (cm, e) => {
413+
if (focusedLinkElement) focusedLinkElement.click();
414+
else e.pick();
415+
}
416+
},
417+
closeOnUnfocus: false
418+
};
419+
420+
if (_cm.options.mode === 'javascript') {
421+
// JavaScript
422+
CodeMirror.showHint(
423+
_cm,
424+
() => {
425+
const c = _cm.getCursor();
426+
const token = _cm.getTokenAt(c);
427+
428+
const hints = this.hinter
429+
.search(token.string)
430+
.filter((h) => h.item.text[0] === token.string[0]);
431+
432+
return {
433+
list: hints,
434+
from: CodeMirror.Pos(c.line, token.start),
435+
to: CodeMirror.Pos(c.line, c.ch)
436+
};
437+
},
438+
hintOptions
439+
);
440+
} else if (_cm.options.mode === 'css') {
441+
// CSS
442+
CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions);
443+
}
444+
}
445+
334446
showReplace() {
335447
this._cm.execCommand('replace');
336448
}
@@ -437,6 +549,7 @@ class Editor extends React.Component {
437549

438550
Editor.propTypes = {
439551
autocloseBracketsQuotes: PropTypes.bool.isRequired,
552+
autocompleteHinter: PropTypes.bool.isRequired,
440553
lineNumbers: PropTypes.bool.isRequired,
441554
lintWarning: PropTypes.bool.isRequired,
442555
linewrap: PropTypes.bool.isRequired,
@@ -482,7 +595,6 @@ Editor.propTypes = {
482595
collapseSidebar: PropTypes.func.isRequired,
483596
expandSidebar: PropTypes.func.isRequired,
484597
clearConsole: PropTypes.func.isRequired,
485-
// showRuntimeErrorWarning: PropTypes.func.isRequired,
486598
hideRuntimeErrorWarning: PropTypes.func.isRequired,
487599
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
488600
provideController: PropTypes.func.isRequired,

client/modules/IDE/components/KeyboardShortcutModal.jsx

+4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ function KeyboardShortcutModal() {
100100
</span>
101101
<span>{t('KeyboardShortcuts.General.TurnOffAccessibleOutput')}</span>
102102
</li>
103+
<li className="keyboard-shortcut-item">
104+
<span className="keyboard-shortcut__command">{'\u21E7'} + Right</span>
105+
<span>Go to Reference for Selected Item in Hinter</span>
106+
</li>
103107
</ul>
104108
</div>
105109
);

client/modules/IDE/components/Preferences/Preferences.unit.test.jsx

+55-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Preferences from './index';
77
* - this.props.fontSize : number
88
* - this.props.autosave : bool
99
* - this.props.autocloseBracketsQuotes : bool
10+
* - this.props.autocompleteHinter : bool
1011
* - this.props.linewrap : bool
1112
* - this.props.lineNumbers : bool
1213
* - this.props.theme : string
@@ -35,6 +36,7 @@ describe('<Preferences />', () => {
3536
fontSize: 12,
3637
autosave: false,
3738
autocloseBracketsQuotes: false,
39+
autocompleteHinter: false,
3840
linewrap: false,
3941
lineNumbers: false,
4042
theme: 'contrast',
@@ -45,6 +47,7 @@ describe('<Preferences />', () => {
4547
setFontSize: jest.fn(),
4648
setAutosave: jest.fn(),
4749
setAutocloseBracketsQuotes: jest.fn(),
50+
setAutocompleteHinter: jest.fn(),
4851
setLinewrap: jest.fn(),
4952
setLineNumbers: jest.fn(),
5053
setTheme: jest.fn(),
@@ -426,6 +429,28 @@ describe('<Preferences />', () => {
426429
);
427430
});
428431

432+
it('autocompleteHinter toggle, starting at false', () => {
433+
// render the component with autocompleteHinter prop set to false
434+
act(() => {
435+
subject();
436+
});
437+
438+
// get ahold of the radio buttons for toggling autocompleteHinter
439+
const autocompleteRadioFalse = screen.getByRole('radio', {
440+
name: /autocomplete hinter off/i
441+
});
442+
const autocompleteRadioTrue = screen.getByRole('radio', {
443+
name: /autocomplete hinter on/i
444+
});
445+
446+
testToggle(
447+
autocompleteRadioFalse,
448+
autocompleteRadioTrue,
449+
props.setAutocompleteHinter,
450+
true
451+
);
452+
});
453+
429454
describe('start autosave value at true', () => {
430455
beforeAll(() => {
431456
props.autosave = true;
@@ -481,6 +506,34 @@ describe('<Preferences />', () => {
481506
});
482507
});
483508

509+
describe('start autocomplete hinter value at true', () => {
510+
beforeAll(() => {
511+
props.autocompleteHinter = true;
512+
});
513+
514+
it('autocompleteHinter toggle, starting at true', () => {
515+
// render the component with autocompleteHinter prop set to true
516+
act(() => {
517+
subject();
518+
});
519+
520+
// get ahold of the radio buttons for toggling autocompleteHinter
521+
const autocompleteRadioFalse = screen.getByRole('radio', {
522+
name: /autocomplete hinter off/i
523+
});
524+
const autocompleteRadioTrue = screen.getByRole('radio', {
525+
name: /autocomplete hinter on/i
526+
});
527+
528+
testToggle(
529+
autocompleteRadioTrue,
530+
autocompleteRadioFalse,
531+
props.setAutocompleteHinter,
532+
false
533+
);
534+
});
535+
});
536+
484537
describe('start linewrap at false', () => {
485538
beforeAll(() => {
486539
props.linewrap = false;
@@ -492,7 +545,7 @@ describe('<Preferences />', () => {
492545
subject();
493546
});
494547

495-
// get ahold of the radio buttons for toggling autocloseBracketsQuotes
548+
// get ahold of the radio buttons for toggling linewrap
496549
const linewrapRadioFalse = screen.getByRole('radio', {
497550
name: /linewrap off/i
498551
});
@@ -520,7 +573,7 @@ describe('<Preferences />', () => {
520573
subject();
521574
});
522575

523-
// get ahold of the radio buttons for toggling autocloseBracketsQuotes
576+
// get ahold of the radio buttons for toggling linewrap
524577
const linewrapRadioFalse = screen.getByRole('radio', {
525578
name: /linewrap off/i
526579
});

0 commit comments

Comments
 (0)