Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ MAILGUN_KEY=<your-mailgun-api-key>
ML5_LIBRARY_USERNAME=ml5
ML5_LIBRARY_EMAIL=examples@ml5js.org
ML5_LIBRARY_PASS=helloml5
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
MONGO_URL=mongodb://127.0.0.1:27017/p5js-web-editor
PORT=8000
PREVIEW_PORT=8002
EDITOR_URL=http://localhost:8000
Expand Down
24 changes: 24 additions & 0 deletions client/modules/IDE/components/Editor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import { FolderIcon } from '../../../../common/icons';
import IconButton from '../../../../common/IconButton';

import contextAwareHinter from '../contextAwareHinter';
import showRenameDialog from '../showRenameDialog';
import { handleRename } from '../rename-variable';

emmet(CodeMirror);

Expand Down Expand Up @@ -171,6 +173,7 @@ class Editor extends React.Component {
},
Enter: 'emmetInsertLineBreak',
Esc: 'emmetResetAbbreviation',
F2: (cm) => this.renameVariable(cm),
[`Shift-Tab`]: false,
[`${metaKey}-Enter`]: () => null,
[`Shift-${metaKey}-Enter`]: () => null,
Expand Down Expand Up @@ -534,6 +537,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) => {
Expand Down
76 changes: 34 additions & 42 deletions client/modules/IDE/components/contextAwareHinter.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import CodeMirror from 'codemirror';
import parseCode from './parseCode';
import parseCodeVariables from './parseCodeVariables';
import classMap from './class-with-methods-map.json';

const scopeMap = require('./finalScopeMap.json');

function formatHintDisplay(name, isBlacklisted) {
return `
<div class="fun-item">
<span class="fun-name">${name}</span>
${
isBlacklisted
? `<div class="inline-warning">⚠️ "${name}" is discouraged in this context.</div>`
: ''
}
</div>
`;
}

function getExpressionBeforeCursor(cm) {
const cursor = cm.getCursor();
const line = cm.getLine(cursor.line);
const uptoCursor = line.slice(0, cursor.ch);

const match = uptoCursor.match(
/([a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*)\.(?:[a-zA-Z_$][\w$]*)?$/
);
Expand All @@ -32,7 +17,6 @@ function getExpressionBeforeCursor(cm) {
export default function contextAwareHinter(cm, options = {}) {
const {
p5ClassMap = {},
varMap = [],
varScopeMap = {},
userFuncMap = {},
userClassMap = {}
Expand Down Expand Up @@ -81,20 +65,15 @@ export default function contextAwareHinter(cm, options = {}) {
}

const to = { line: cursor.line, ch: cursor.ch };
let tokenLength = 0;
if (dotMatch) {
const typed = dotMatch[1] || ''; // what's typed after the dot
tokenLength = typed.length;
}

const typed = dotMatch?.[1]?.toLowerCase() || '';

const methodHints = methods
.filter((method) => method.toLowerCase().startsWith(typed))
.map((method) => ({
item: {
text: method,
type: 'fun'
type: 'fun',
isMethod: true
},
displayText: method,
from,
Expand All @@ -111,34 +90,48 @@ export default function contextAwareHinter(cm, options = {}) {
const currentContext = parseCode(cm);
const allHints = hinter.search(currentWord);

const whitelist = scopeMap[currentContext]?.whitelist || [];
// const whitelist = scopeMap[currentContext]?.whitelist || [];
const blacklist = scopeMap[currentContext]?.blacklist || [];

const lowerCurrentWord = currentWord.toLowerCase();

function isInScope(varName) {
const varScope = varScopeMap[varName];
if (!varScope) return false;
if (varScope === 'global') return true;
if (varScope === currentContext) return true;
return false;
return Object.entries(varScopeMap).some(
([scope, vars]) =>
vars.has(varName) && (scope === 'global' || scope === currentContext)
);
}

const varHints = varMap
const allVarNames = Array.from(
new Set(
Object.values(varScopeMap)
.map((s) => Array.from(s)) // convert Set to Array
.flat()
.filter((name) => typeof name === 'string')
)
);

const varHints = allVarNames
.filter(
(varName) =>
varName.toLowerCase().startsWith(lowerCurrentWord) && isInScope(varName)
)
.map((varName) => ({
item: {
text: varName,
type: userFuncMap[varName] ? 'fun' : 'var'
},
isBlacklisted: blacklist.includes(varName),
displayText: formatHintDisplay(varName, blacklist.includes(varName)),
from: { line, ch },
to: { line, ch: ch - currentWord.length }
}));
.map((varName) => {
const isFunc = !!userFuncMap[varName];
const baseItem = isFunc
? { ...userFuncMap[varName] }
: {
text: varName,
type: 'var',
params: [],
p5: false
};

return {
item: baseItem,
isBlacklisted: blacklist.includes(varName)
};
});

const filteredHints = allHints
.filter(
Expand All @@ -154,8 +147,7 @@ export default function contextAwareHinter(cm, options = {}) {

return {
...hint,
isBlacklisted,
displayText: formatHintDisplay(name, isBlacklisted)
isBlacklisted
};
});

Expand Down
5 changes: 5 additions & 0 deletions client/modules/IDE/components/finalScopeMap.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@
"loadTable",
"loadXML",
"soundFormats"
],
"blacklist":[
"preload",
"setup",
"draw"
]
},
"doubleClicked": {
Expand Down
26 changes: 12 additions & 14 deletions client/modules/IDE/components/parseCode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const acorn = require('acorn');
const walk = require('acorn-walk');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

export default function parseCode(_cm) {
const code = _cm.getValue();
Expand All @@ -8,37 +8,35 @@ export default function parseCode(_cm) {

let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 'latest',
sourceType: 'script'
ast = parser.parse(code, {
sourceType: 'script',
plugins: ['jsx', 'typescript'] // add plugins as needed
});
} catch (e) {
console.warn('Failed to parse code', e.message);
// console.warn('Failed to parse code with Babel:', e.message);
return 'global';
}

let context = 'global';

walk.fullAncestor(ast, (node, ancestors) => {
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
traverse(ast, {
Function(path) {
const { node } = path;
if (offset >= node.start && offset <= node.end) {
if (node.id && node.id.name) {
context = node.id.name;
} else {
const parent = ancestors[ancestors.length - 2];
const parent = path.parentPath.node;
if (
parent?.type === 'VariableDeclarator' &&
parent.type === 'VariableDeclarator' &&
parent.id.type === 'Identifier'
) {
context = parent.id.name;
} else {
context = '(anonymous)';
}
}
path.stop(); // Stop traversal once we found the function context
}
}
});
Expand Down
46 changes: 0 additions & 46 deletions client/modules/IDE/components/parseCodeBabel.js

This file was deleted.

Loading