-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Final GSoC Work: Context-Aware Autocomplete, Renaming, and Refactoring Enhancements #3594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
a79c2f1
3e84dd5
5fc1c64
9c4bc52
cf71058
e8bf752
819b9cb
5559967
2c132e6
761657a
b0ec598
87d89a5
d28914a
b0a0feb
5120dce
bdb7d86
e3c7f18
093dccc
ea39d0e
a627fde
990712a
b006d5d
f94b2fb
a7229da
f30b884
01454d0
a67e89d
ce4ae68
2415953
1779145
c50566f
88a9c65
9172902
0665cc6
293b49e
842c46b
a0569d0
fdcd37a
26442f4
ca26d11
77465e9
078b882
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# GSoC 2025: p5.js Autocomplete Hinter & Refactoring System | ||
This readme elaborates on the core components of the context-aware autocomplete hinter, refactoring utilities, and supporting data structures developed as part of Google Summer of Code 2025. The goal is to enable smart context-aware autocompletion, jump-to-definition, and safe variable renaming. | ||
|
||
# Project Overview | ||
|
||
## Autocomplete Hinter Context-Aware Functionality | ||
The following files and modules work together to make the p5.js autocomplete hinter context-aware: | ||
|
||
### p5CodeAstAnalyzer.js | ||
Purpose: Parses user-written p5.js code using Babel and extracts structural information: | ||
|
||
- Maps variable names to p5 class instances | ||
- Tracks declared variables in each function or global scope | ||
- Detects user-defined functions and their parameters | ||
- Collects info about user-defined classes, constructor-assigned properties, and methods | ||
|
||
Key Output Maps: | ||
|
||
- variableToP5ClassMap: Maps variable names (e.g., col) to their p5.js class type (e.g., p5.Color) | ||
- scopeToDeclaredVarsMap: Maps function names or global scope to variables declared in them | ||
- userDefinedFunctionMetadata: Metadata about custom functions (params, type, etc.) | ||
- userDefinedClassMetadata: Metadata for user-defined classes (methods, constructor properties) | ||
|
||
### context-aware-hinter.js | ||
Purpose: Provides code autocompletion hints based on: | ||
|
||
- Current cursor context (draw, setup, etc.) | ||
- p5CodeAstAnalyzer output | ||
- p5 class method definitions | ||
- Variable/function scope and visibility | ||
- Scope-specific blacklist/whitelist logic | ||
|
||
Features: | ||
|
||
- Dot-autocompletion (e.g., col. shows methods of p5.Color) | ||
- Scope-sensitive variable/function suggestions | ||
- Ranks hints by type and scope relevance | ||
|
||
### getContext.js | ||
Purpose: Get the context of the cursor, i.e. inside what function is the cursor in | ||
|
||
## Context-Aware Renaming Functionality | ||
The following files ensure context-aware renaming when a variable or user-defined function is selected and the F2 button is clicked | ||
|
||
### rename-variable.js | ||
Purpose: Safely renames a variable in the user's code editor by: | ||
|
||
- Analyzing AST to find all matching identifiers | ||
- Ensuring replacement only occurs within the same lexical scope | ||
- Performing in-place replacement using CodeMirror APIs | ||
|
||
### showRenameDialog.jsx | ||
Purpose: Opens either a dialog box to get the new variable name or a temporary box to show that the word selected cannot be renamed | ||
|
||
## Jump to Definition | ||
The following file allows user to jump to the definition for variables or parameters when a word is ctrl-clicked. | ||
|
||
### jumptodefinition.js | ||
Purpose: Implements “jump to definition” for variables or parameters in the editor. | ||
|
||
How It Works: | ||
|
||
- Uses AST + scope map to locate the definition site of a variable | ||
- Supports both VariableDeclarator and FunctionDeclaration/params | ||
- Moves the editor cursor to the source location of the definition | ||
|
||
## Supporting Data Files | ||
### p5-instance-methods-and-creators.json | ||
Purpose: Maps p5.js classes to: | ||
|
||
- Methods used to instantiate them (createMethods) | ||
- Methods available on those instances (methods) | ||
|
||
### p5-scope-function-access-map.json | ||
Purpose: Defines which p5.js functions are allowed or disallowed inside functions like setup, draw, preload, etc. | ||
|
||
### p5-reference-functions.json | ||
Purpose: A flat list of all available p5.js functions. | ||
|
||
Used to: | ||
|
||
- Differentiate between built-in and user-defined functions | ||
- Filter out redefinitions or incorrect hints | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,3 +53,5 @@ render( | |
</Provider>, | ||
document.getElementById('root') | ||
); | ||
|
||
export default store; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since it seems like |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,6 +73,12 @@ import { EditorContainer, EditorHolder } from './MobileEditor'; | |
import { FolderIcon } from '../../../../common/icons'; | ||
import IconButton from '../../../../common/IconButton'; | ||
|
||
import contextAwareHinter from '../contextAwareHinter'; | ||
import showRenameDialog from '../showRenameDialog'; | ||
import { handleRename } from '../rename-variable'; | ||
import { jumpToDefinition } from '../jump-to-definition'; | ||
import { ensureAriaLiveRegion } from '../../utils/ScreenReaderHelper'; | ||
|
||
emmet(CodeMirror); | ||
|
||
window.JSHINT = JSHINT; | ||
|
@@ -109,6 +115,7 @@ class Editor extends React.Component { | |
|
||
componentDidMount() { | ||
this.beep = new Audio(beepUrl); | ||
ensureAriaLiveRegion(); | ||
// this.widgets = []; | ||
this._cm = CodeMirror(this.codemirrorContainer, { | ||
theme: `p5-${this.props.theme}`, | ||
|
@@ -154,6 +161,16 @@ class Editor extends React.Component { | |
|
||
delete this._cm.options.lint.options.errors; | ||
|
||
this._cm.getWrapperElement().addEventListener('click', (e) => { | ||
const isMac = /Mac/.test(navigator.platform); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to use this pre-existing device util here? |
||
const isCtrlClick = isMac ? e.metaKey : e.ctrlKey; | ||
|
||
if (isCtrlClick) { | ||
const pos = this._cm.coordsChar({ left: e.clientX, top: e.clientY }); | ||
jumpToDefinition.call(this, pos); | ||
} | ||
}); | ||
|
||
const replaceCommand = | ||
metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`; | ||
this._cm.setOption('extraKeys', { | ||
|
@@ -172,6 +189,7 @@ class Editor extends React.Component { | |
[`Shift-${metaKey}-E`]: (cm) => { | ||
cm.getInputField().blur(); | ||
}, | ||
F2: (cm) => this.renameVariable(cm), | ||
[`Shift-Tab`]: false, | ||
[`${metaKey}-Enter`]: () => null, | ||
[`Shift-${metaKey}-Enter`]: () => null, | ||
|
@@ -209,7 +227,13 @@ class Editor extends React.Component { | |
} | ||
|
||
this._cm.on('keydown', (_cm, e) => { | ||
// Show hint | ||
if ( | ||
((e.ctrlKey || e.metaKey) && e.key === 'v') || | ||
e.ctrlKey || | ||
e.altKey | ||
) { | ||
return; | ||
} | ||
const mode = this._cm.getOption('mode'); | ||
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) { | ||
this.showHint(_cm); | ||
|
@@ -395,12 +419,15 @@ class Editor extends React.Component { | |
} | ||
|
||
showHint(_cm) { | ||
if (!_cm) return; | ||
|
||
if (!this.props.autocompleteHinter) { | ||
CodeMirror.showHint(_cm, () => {}, {}); | ||
return; | ||
} | ||
|
||
let focusedLinkElement = null; | ||
|
||
const setFocusedLinkElement = (set) => { | ||
if (set && !focusedLinkElement) { | ||
const activeItemLink = document.querySelector( | ||
|
@@ -415,6 +442,7 @@ class Editor extends React.Component { | |
} | ||
} | ||
}; | ||
|
||
const removeFocusedLinkElement = () => { | ||
if (focusedLinkElement) { | ||
focusedLinkElement.classList.remove('focused-hint-link'); | ||
|
@@ -437,12 +465,8 @@ class Editor extends React.Component { | |
); | ||
if (activeItemLink) activeItemLink.click(); | ||
}, | ||
Right: (cm, e) => { | ||
setFocusedLinkElement(true); | ||
}, | ||
Left: (cm, e) => { | ||
removeFocusedLinkElement(); | ||
}, | ||
Right: (cm, e) => setFocusedLinkElement(true), | ||
Left: (cm, e) => removeFocusedLinkElement(), | ||
Up: (cm, e) => { | ||
const onLink = removeFocusedLinkElement(); | ||
e.moveFocus(-1); | ||
|
@@ -461,30 +485,28 @@ class Editor extends React.Component { | |
closeOnUnfocus: false | ||
}; | ||
|
||
if (_cm.options.mode === 'javascript') { | ||
// JavaScript | ||
CodeMirror.showHint( | ||
_cm, | ||
() => { | ||
const c = _cm.getCursor(); | ||
const token = _cm.getTokenAt(c); | ||
|
||
const hints = this.hinter | ||
.search(token.string) | ||
.filter((h) => h.item.text[0] === token.string[0]); | ||
|
||
return { | ||
list: hints, | ||
from: CodeMirror.Pos(c.line, token.start), | ||
to: CodeMirror.Pos(c.line, c.ch) | ||
}; | ||
}, | ||
hintOptions | ||
); | ||
} else if (_cm.options.mode === 'css') { | ||
// CSS | ||
CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions); | ||
} | ||
const triggerHints = () => { | ||
if (_cm.options.mode === 'javascript') { | ||
CodeMirror.showHint( | ||
_cm, | ||
() => { | ||
const c = _cm.getCursor(); | ||
const token = _cm.getTokenAt(c); | ||
const hints = contextAwareHinter(_cm, { hinter: this.hinter }); | ||
return { | ||
list: hints, | ||
from: CodeMirror.Pos(c.line, token.start), | ||
to: CodeMirror.Pos(c.line, c.ch) | ||
}; | ||
}, | ||
hintOptions | ||
); | ||
} else if (_cm.options.mode === 'css') { | ||
CodeMirror.showHint(_cm, CodeMirror.hint.css, hintOptions); | ||
} | ||
}; | ||
|
||
setTimeout(triggerHints, 0); | ||
} | ||
|
||
showReplace() { | ||
|
@@ -522,6 +544,27 @@ class Editor extends React.Component { | |
} | ||
} | ||
|
||
renameVariable(cm) { | ||
const cursorCoords = cm.cursorCoords(true, 'page'); | ||
const selection = cm.getSelection(); | ||
const pos = cm.getCursor(); // or selection start | ||
const token = cm.getTokenAt(pos); | ||
const tokenType = token.type; | ||
if (!selection) { | ||
return; | ||
} | ||
|
||
const sel = cm.listSelections()[0]; | ||
const fromPos = | ||
CodeMirror.cmpPos(sel.anchor, sel.head) <= 0 ? sel.anchor : sel.head; | ||
|
||
showRenameDialog(tokenType, cursorCoords, selection, (newName) => { | ||
if (newName && newName.trim() !== '' && newName !== selection) { | ||
handleRename(fromPos, selection, newName, cm); | ||
} | ||
}); | ||
} | ||
|
||
initializeDocuments(files) { | ||
this._docs = {}; | ||
files.forEach((file) => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep the README as a separate GitHub Gist, and not include it in the final PR to be merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with @diyaayay here!
It'd be great if you could move this into the
contributor_docs
folder, and I can update how it's organized and referenced afterwards!