Skip to content

Commit 7392453

Browse files
authored
Merge pull request #3594 from kammeows/develop
Final GSoC Work: Context-Aware Autocomplete, Renaming, and Refactoring Enhancements
2 parents 8602dd7 + 4bfbaeb commit 7392453

24 files changed

+5811
-4373
lines changed

client/index.jsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,19 @@ const App = () => {
9393
);
9494
};
9595

96-
render(
97-
<Provider store={store}>
98-
<ThemeProvider>
99-
<Suspense fallback={<Loader />}>
100-
<App />
101-
</Suspense>
102-
</ThemeProvider>
103-
</Provider>,
104-
document.getElementById('root')
105-
);
96+
// This prevents crashes in test environments (like Jest) where document.getElementById('root') may return null.
97+
const rootEl = document.getElementById('root');
98+
if (rootEl) {
99+
render(
100+
<Provider store={store}>
101+
<ThemeProvider>
102+
<Suspense fallback={<Loader />}>
103+
<App />
104+
</Suspense>
105+
</ThemeProvider>
106+
</Provider>,
107+
rootEl
108+
);
109+
}
110+
111+
export default store;

client/modules/IDE/components/Editor/index.jsx

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ import { EditorContainer, EditorHolder } from './MobileEditor';
7373
import { FolderIcon } from '../../../../common/icons';
7474
import { IconButton } from '../../../../common/IconButton';
7575

76+
import contextAwareHinter from '../../../../utils/contextAwareHinter';
77+
import showRenameDialog from '../../../../utils/showRenameDialog';
78+
import handleRename from '../../../../utils/rename-variable';
79+
import { jumpToDefinition } from '../../../../utils/jump-to-definition';
80+
import { ensureAriaLiveRegion } from '../../../../utils/ScreenReaderHelper';
81+
import { isMac } from '../../../../utils/device';
82+
7683
emmet(CodeMirror);
7784

7885
window.JSHINT = JSHINT;
@@ -109,6 +116,7 @@ class Editor extends React.Component {
109116

110117
componentDidMount() {
111118
this.beep = new Audio(beepUrl);
119+
ensureAriaLiveRegion();
112120
// this.widgets = [];
113121
this._cm = CodeMirror(this.codemirrorContainer, {
114122
theme: `p5-${this.props.theme}`,
@@ -154,6 +162,17 @@ class Editor extends React.Component {
154162

155163
delete this._cm.options.lint.options.errors;
156164

165+
this._cm.getWrapperElement().addEventListener('click', (e) => {
166+
const isCtrlClick = isMac() ? e.metaKey : e.ctrlKey;
167+
168+
if (isCtrlClick) {
169+
const pos = this._cm.coordsChar({ left: e.clientX, top: e.clientY });
170+
jumpToDefinition.call(this, pos);
171+
}
172+
});
173+
174+
const renameKey = isMac() ? 'Ctrl-F2' : 'F2';
175+
157176
const replaceCommand =
158177
metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`;
159178
this._cm.setOption('extraKeys', {
@@ -172,6 +191,7 @@ class Editor extends React.Component {
172191
[`Shift-${metaKey}-E`]: (cm) => {
173192
cm.getInputField().blur();
174193
},
194+
[renameKey]: (cm) => this.renameVariable(cm),
175195
[`Shift-Tab`]: false,
176196
[`${metaKey}-Enter`]: () => null,
177197
[`Shift-${metaKey}-Enter`]: () => null,
@@ -209,7 +229,14 @@ class Editor extends React.Component {
209229
}
210230

211231
this._cm.on('keydown', (_cm, e) => {
212-
// Show hint
232+
// Skip hinting if the user is pasting (Ctrl/Cmd+V) or using modifier keys (Ctrl/Alt)
233+
if (
234+
((e.ctrlKey || e.metaKey) && e.key === 'v') ||
235+
e.ctrlKey ||
236+
e.altKey
237+
) {
238+
return;
239+
}
213240
const mode = this._cm.getOption('mode');
214241
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) {
215242
this.showHint(_cm);
@@ -395,12 +422,15 @@ class Editor extends React.Component {
395422
}
396423

397424
showHint(_cm) {
425+
if (!_cm) return;
426+
398427
if (!this.props.autocompleteHinter) {
399428
CodeMirror.showHint(_cm, () => {}, {});
400429
return;
401430
}
402431

403432
let focusedLinkElement = null;
433+
404434
const setFocusedLinkElement = (set) => {
405435
if (set && !focusedLinkElement) {
406436
const activeItemLink = document.querySelector(
@@ -415,6 +445,7 @@ class Editor extends React.Component {
415445
}
416446
}
417447
};
448+
418449
const removeFocusedLinkElement = () => {
419450
if (focusedLinkElement) {
420451
focusedLinkElement.classList.remove('focused-hint-link');
@@ -437,12 +468,8 @@ class Editor extends React.Component {
437468
);
438469
if (activeItemLink) activeItemLink.click();
439470
},
440-
Right: (cm, e) => {
441-
setFocusedLinkElement(true);
442-
},
443-
Left: (cm, e) => {
444-
removeFocusedLinkElement();
445-
},
471+
Right: (cm, e) => setFocusedLinkElement(true),
472+
Left: (cm, e) => removeFocusedLinkElement(),
446473
Up: (cm, e) => {
447474
const onLink = removeFocusedLinkElement();
448475
e.moveFocus(-1);
@@ -461,30 +488,28 @@ class Editor extends React.Component {
461488
closeOnUnfocus: false
462489
};
463490

464-
if (_cm.options.mode === 'javascript') {
465-
// JavaScript
466-
CodeMirror.showHint(
467-
_cm,
468-
() => {
469-
const c = _cm.getCursor();
470-
const token = _cm.getTokenAt(c);
471-
472-
const hints = this.hinter
473-
.search(token.string)
474-
.filter((h) => h.item.text[0] === token.string[0]);
475-
476-
return {
477-
list: hints,
478-
from: CodeMirror.Pos(c.line, token.start),
479-
to: CodeMirror.Pos(c.line, c.ch)
480-
};
481-
},
482-
hintOptions
483-
);
484-
} else if (_cm.options.mode === 'css') {
485-
// CSS
486-
CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions);
487-
}
491+
const triggerHints = () => {
492+
if (_cm.options.mode === 'javascript') {
493+
CodeMirror.showHint(
494+
_cm,
495+
() => {
496+
const c = _cm.getCursor();
497+
const token = _cm.getTokenAt(c);
498+
const hints = contextAwareHinter(_cm, { hinter: this.hinter });
499+
return {
500+
list: hints,
501+
from: CodeMirror.Pos(c.line, token.start),
502+
to: CodeMirror.Pos(c.line, c.ch)
503+
};
504+
},
505+
hintOptions
506+
);
507+
} else if (_cm.options.mode === 'css') {
508+
CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions);
509+
}
510+
};
511+
512+
setTimeout(triggerHints, 0);
488513
}
489514

490515
showReplace() {
@@ -522,6 +547,34 @@ class Editor extends React.Component {
522547
}
523548
}
524549

550+
renameVariable(cm) {
551+
const cursorCoords = cm.cursorCoords(true, 'page');
552+
const selection = cm.getSelection();
553+
const pos = cm.getCursor(); // or selection start
554+
const token = cm.getTokenAt(pos);
555+
const tokenType = token.type;
556+
if (!selection) {
557+
return;
558+
}
559+
560+
const sel = cm.listSelections()[0];
561+
const fromPos =
562+
CodeMirror.cmpPos(sel.anchor, sel.head) <= 0 ? sel.anchor : sel.head;
563+
564+
showRenameDialog(
565+
cm,
566+
fromPos,
567+
tokenType,
568+
cursorCoords,
569+
selection,
570+
(newName) => {
571+
if (newName && newName.trim() !== '' && newName !== selection) {
572+
handleRename(fromPos, selection, newName, cm);
573+
}
574+
}
575+
);
576+
}
577+
525578
initializeDocuments(files) {
526579
this._docs = {};
527580
files.forEach((file) => {

client/modules/IDE/components/KeyboardShortcutModal.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function KeyboardShortcutModal() {
88
metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`;
99
const newFileCommand =
1010
metaKey === 'Ctrl' ? `${metaKeyName} + Alt + N` : `${metaKeyName} + ⌥ + N`;
11+
const renameCommand = metaKey === 'Ctrl' ? 'F2' : 'Ctrl + F2';
1112
return (
1213
<div className="keyboard-shortcuts">
1314
<h3 className="keyboard-shortcuts__title">
@@ -75,6 +76,10 @@ function KeyboardShortcutModal() {
7576
<span className="keyboard-shortcut__command">{newFileCommand}</span>
7677
<span>{t('KeyboardShortcuts.CodeEditing.CreateNewFile')}</span>
7778
</li>
79+
<li className="keyboard-shortcut-item">
80+
<span className="keyboard-shortcut__command">{renameCommand}</span>
81+
<span>{t('KeyboardShortcuts.CodeEditing.RenameVariable')}</span>
82+
</li>
7883
</ul>
7984
<h3 className="keyboard-shortcuts__title">
8085
{t('KeyboardShortcuts.General')}

0 commit comments

Comments
 (0)