Skip to content

Commit 159e319

Browse files
authored
Add Features: Refactoring and Variable Highlighting (source-academy#1037)
* Add Refactoring functionality: Ctrl-M/Cmd-M * Add variable highlighting
1 parent de93897 commit 159e319

File tree

5 files changed

+86
-1
lines changed

5 files changed

+86
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/***
2+
* We need access to the Range class in the Ace Editor to create our own ranges, and
3+
* this is the only way to do it.
4+
*/
5+
const ace = (() => {
6+
return (window as any).ace;
7+
})() as any;
8+
const { Range } = ace.acequire('ace/range');
9+
export default Range;

src/components/workspace/Editor.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import sharedbAce from 'sharedb-ace';
55

66
import 'ace-builds/src-noconflict/ext-language_tools';
77
import 'ace-builds/src-noconflict/ext-searchbox';
8+
import { createContext, getAllOccurrencesInScope } from 'js-slang';
89
import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source';
910
import 'js-slang/dist/editors/ace/theme/source';
1011
import { LINKS } from '../../utils/constants';
12+
import AceRange from './AceRange';
1113
import { checkSessionIdExists } from './collabEditing/helper';
1214

1315
/**
@@ -45,18 +47,21 @@ export interface IPosition {
4547
class Editor extends React.PureComponent<IEditorProps, {}> {
4648
public ShareAce: any;
4749
public AceEditor: React.RefObject<AceEditor>;
50+
private markerIds: number[];
4851
private onChangeMethod: (newCode: string) => void;
4952
private onValidateMethod: (annotations: IAnnotation[]) => void;
5053

5154
constructor(props: IEditorProps) {
5255
super(props);
5356
this.AceEditor = React.createRef();
5457
this.ShareAce = null;
58+
this.markerIds = [];
5559
this.onChangeMethod = (newCode: string) => {
5660
if (this.props.handleUpdateHasUnsavedChanges) {
5761
this.props.handleUpdateHasUnsavedChanges(true);
5862
}
5963
this.props.handleEditorValueChange(newCode);
64+
this.handleVariableHighlighting();
6065
};
6166
this.onValidateMethod = (annotations: IAnnotation[]) => {
6267
if (this.props.isEditorAutorun && annotations.length === 0) {
@@ -116,6 +121,8 @@ class Editor extends React.PureComponent<IEditorProps, {}> {
116121
if (this.props.editorSessionId !== '') {
117122
this.handleStartCollabEditing(editor);
118123
}
124+
125+
this.handleVariableHighlighting();
119126
}
120127

121128
public componentWillUnmount() {
@@ -181,6 +188,14 @@ class Editor extends React.PureComponent<IEditorProps, {}> {
181188
mac: 'Command-B'
182189
},
183190
exec: this.handleNavigate
191+
},
192+
{
193+
name: 'refactor',
194+
bindKey: {
195+
win: 'Ctrl-M',
196+
mac: 'Command-M'
197+
},
198+
exec: this.handleRefactor
184199
}
185200
]}
186201
editorProps={{
@@ -193,6 +208,7 @@ class Editor extends React.PureComponent<IEditorProps, {}> {
193208
highlightActiveLine={false}
194209
mode={this.chapterNo()} // select according to props.sourceChapter
195210
onChange={this.onChangeMethod}
211+
onCursorChange={this.handleVariableHighlighting}
196212
onValidate={this.onValidateMethod}
197213
theme="source"
198214
value={this.props.editorValue}
@@ -230,6 +246,59 @@ class Editor extends React.PureComponent<IEditorProps, {}> {
230246
}
231247
};
232248

249+
private handleRefactor = () => {
250+
const editor = (this.AceEditor.current as any).editor;
251+
if (!editor) {
252+
return;
253+
}
254+
const code = this.props.editorValue;
255+
const chapter = this.props.sourceChapter;
256+
const position = editor.getCursorPosition();
257+
258+
const sourceLocations = getAllOccurrencesInScope(code, createContext(chapter), {
259+
line: position.row + 1, // getCursorPosition returns 0-indexed row, function here takes in 1-indexed row
260+
column: position.column
261+
});
262+
263+
const selection = editor.getSelection();
264+
const ranges = sourceLocations.map(
265+
loc => new AceRange(loc.start.line - 1, loc.start.column, loc.end.line - 1, loc.end.column)
266+
);
267+
ranges.forEach(range => selection.addRange(range));
268+
};
269+
270+
private handleVariableHighlighting = () => {
271+
// using Ace Editor's way of highlighting as seen here: https://github.com/ajaxorg/ace/blob/master/lib/ace/editor.js#L497
272+
// We use async blocks so we don't block the browser during editing
273+
274+
setTimeout(() => {
275+
const editor = (this.AceEditor.current as any).editor;
276+
const session = editor.session;
277+
const code = this.props.editorValue;
278+
const chapterNumber = this.props.sourceChapter;
279+
const position = editor.getCursorPosition();
280+
if (!session || !session.bgTokenizer) {
281+
return;
282+
}
283+
this.markerIds.forEach(id => {
284+
session.removeMarker(id);
285+
});
286+
const ranges = getAllOccurrencesInScope(code, createContext(chapterNumber), {
287+
line: position.row + 1,
288+
column: position.column
289+
}).map(
290+
loc => new AceRange(loc.start.line - 1, loc.start.column, loc.end.line - 1, loc.end.column)
291+
);
292+
293+
const markerType = 'ace_variable_highlighting';
294+
const markerIds = ranges.map(range => {
295+
// returns the marker ID for removal later
296+
return session.addMarker(range, markerType, 'text');
297+
});
298+
this.markerIds = markerIds;
299+
}, 10);
300+
};
301+
233302
private handleGutterClick = (e: any) => {
234303
const target = e.domEvent.target;
235304
if (

src/components/workspace/__tests__/__snapshots__/Editor.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`Editor renders correctly 1`] = `
44
"<HotKeys className=\\"Editor\\" handlers={{...}}>
55
<div className=\\"row editor-react-ace\\">
6-
<ReactAce className=\\"react-ace\\" commands={{...}} editorProps={{...}} markers={{...}} fontSize={17} height=\\"100%\\" highlightActiveLine={false} mode=\\"source1\\" onChange={[Function]} onValidate={[Function]} theme=\\"source\\" value=\\"\\" width=\\"100%\\" setOptions={{...}} name=\\"ace-editor\\" focus={false} enableSnippets={false} showGutter={true} onPaste={{...}} onLoad={{...}} onScroll={{...}} minLines={{...}} maxLines={{...}} readOnly={false} showPrintMargin={true} tabSize={4} cursorStart={1} style={{...}} scrollMargin={{...}} wrapEnabled={false} enableBasicAutocompletion={false} enableLiveAutocompletion={false} placeholder={{...}} navigateToFileEnd={true} />
6+
<ReactAce className=\\"react-ace\\" commands={{...}} editorProps={{...}} markers={{...}} fontSize={17} height=\\"100%\\" highlightActiveLine={false} mode=\\"source1\\" onChange={[Function]} onCursorChange={[Function]} onValidate={[Function]} theme=\\"source\\" value=\\"\\" width=\\"100%\\" setOptions={{...}} name=\\"ace-editor\\" focus={false} enableSnippets={false} showGutter={true} onPaste={{...}} onLoad={{...}} onScroll={{...}} minLines={{...}} maxLines={{...}} readOnly={false} showPrintMargin={true} tabSize={4} cursorStart={1} style={{...}} scrollMargin={{...}} wrapEnabled={false} enableBasicAutocompletion={false} enableLiveAutocompletion={false} placeholder={{...}} navigateToFileEnd={true} />
77
</div>
88
</HotKeys>"
99
`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.ace_variable_highlighting {
2+
z-index: 4;
3+
position: absolute;
4+
box-sizing: border-box;
5+
border: 1px dashed rgba(255, 255, 255, 0.6);
6+
}

src/styles/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
@import 'playground';
3131
@import 'sourcereel';
3232
@import 'sourcecast';
33+
@import 'variableHighlighting';
3334
@import 'workspaceGreen';
3435
@import 'workspace';

0 commit comments

Comments
 (0)