UNPKG

three-codeeditor

Version:

codeeditor for three.js

364 lines (310 loc) 14.5 kB
// This is an extension for THREE.CodeEditor ;(function(exports) { // imports var oop = ace.require("ace/lib/oop"); var fun = lively.lang.fun; var AutoComplete = ace.require("ace/autocomplete").Autocomplete; var FilteredList = ace.require("ace/autocomplete").FilteredList; // exports exports.installDynamicJSCompleterInto = installDynamicJSCompleterInto; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function installDynamicJSCompleterInto(aceEditor) { var completer = { isDynamicJSCompleter: true, getCompletions: function(editor, session, pos, prefix, thenDo) { var result = dynamicCompleter.getCompletions( getSelectionOrLineString(editor, pos), function(code) { var evaled = lively.vm.syncEval(code, {topLevelVarRecorder: {}, sourceURL: "completions-"+Date.now()}); return evaled instanceof Error ? null : evaled; }); if (!result || !result.completions) return thenDo(null, []); thenDo(null, result.completions.reduce(function(completions, group) { var groupName = lively.lang.string.truncate(group[0], 20); return completions.concat(group[1].map(function(compl) { return { caption: "[" + groupName+ "] " + compl, value: compl, score: 210, meta: "dynamic", completer: compl[0] !== "[" ? null : { insertMatch: function(ed, completion) { var pos = ed.getCursorPosition(); var dotRange = ed.find(".", { start: ed.getCursorPosition(), preventScroll: true, backwards: true }); if (dotRange.start.row === pos.row) { // remove everything until (including) the "." before inserting completion ed.session.replace({start: dotRange.start, end: pos}, ""); } ed.execCommand("insertstring", completion.value || completion); } } } })) }, [])); // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function getSelectionOrLineString(ed, pos) { var range = ed.selection.getRange() if (range.isEmpty()) range = ed.selection.getLineRange(pos.row, true); return ed.session.getTextRange(range); } } } aceEditor.completers = (aceEditor.completers || []) .filter(function(ea) { return !ea.isDynamicJSCompleter; }) .concat([completer]); }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // dynamic JavaScript completer // -=-=-=-=-=-=-=-=-=-=-=-=-=-=- // FIXME this should go into lively.vm! var dynamicCompleter = { getCompletions: function(code, evalFunc) { var err, completions getCompletions(evalFunc, code, function(e, c, pre) { err = e, completions = {prefix: pre, completions: c}; }) if (err) { alert(err); return {error: String(err.stack || err), prefix: '', completions: []}; } else return completions; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // rk 2013-10-10 I extracted the code below into a nodejs module (since this // stuff is also useful on a server and in other contexts). Right now we have no // good way to load nodejs modules into Lively and I inline the code here. Please // fix soon! // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // helper function signatureOf(name, func) { var source = String(func), match = source.match(/function\s*[a-zA-Z0-9_$]*\s*\(([^\)]*)\)/), params = (match && match[1]) || ''; return makeValidCompletion(name) + '(' + params + ')'; } function isClass(obj) { if (obj === obj || obj === Array || obj === Function || obj === String || obj === Boolean || obj === Date || obj === RegExp || obj === Number) return true; return (obj instanceof Function) && ((obj.superclass !== undefined) || (obj._superclass !== undefined)); } function pluck(list, prop) { return list.map(function(ea) { return ea[prop]; }); } function getObjectForCompletion(evalFunc, stringToEval, thenDo) { // thenDo = function(err, obj, startLetters) var idx = stringToEval.lastIndexOf('.'), startLetters = ''; if (idx >= 0) { startLetters = stringToEval.slice(idx+1); stringToEval = stringToEval.slice(0,idx); } else { startLetters = stringToEval; stringToEval = 'Global'; } var completions = []; try { var obj = evalFunc(stringToEval); } catch (e) { thenDo(e, null, null); } thenDo(null, obj, startLetters); } function propertyExtract(excludes, obj, extractor) { // show(''+excludes) return Object.getOwnPropertyNames(obj) .filter(function(key) { return excludes.indexOf(key) === -1; }) .map(extractor) .filter(function(ea) { return !!ea; }) .sort(function(a,b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); }); } function isValidIdentifier(string) { // FIXME real identifier test is more complex... return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(string); } function makeValidCompletion(string) { return isValidIdentifier(string) ? string : "['" + string.replace(/'/g, "\\'") + "']"; } function getMethodsOf(excludes, obj) { return propertyExtract(excludes, obj, function(key) { if ((obj.__lookupGetter__ && obj.__lookupGetter__(key)) || typeof obj[key] !== 'function') return null; return {name: key, completion: signatureOf(key, obj[key])}; }) } function getAttributesOf(excludes, obj) { return propertyExtract(excludes, obj, function(key) { if ((obj.__lookupGetter__ && !obj.__lookupGetter__(key)) && typeof obj[key] === 'function') return null; return {name: key, completion: makeValidCompletion(key)}; }) } function getProtoChain(obj) { var protos = [], proto = obj; while (obj) { protos.push(obj); obj = obj.__proto__ } return protos; } function getDescriptorOf(originalObj, proto) { function shorten(s, len) { if (s.length > len) s = s.slice(0,len) + '...'; return s.replace(/\n/g, '').replace(/\s+/g, ' '); } var stringified; try { stringified = String(originalObj); } catch (e) { stringified = "{/*...*/}"; } if (originalObj === proto) { if (typeof originalObj !== 'function') return shorten(stringified, 50); var funcString = stringified, body = shorten(funcString.slice(funcString.indexOf('{')+1, funcString.lastIndexOf('}')), 50); return signatureOf(originalObj.displayName || originalObj.name || 'function', originalObj) + ' {' + body + '}'; } var klass = proto.hasOwnProperty('constructor') && proto.constructor; if (!klass) return 'prototype'; if (typeof klass.type === 'string' && klass.type.length) return shorten(klass.type, 50); if (typeof klass.name === 'string' && klass.name.length) return shorten(klass.name, 50); return "anonymous class"; } function getCompletionsOfObj(obj, thenDo) { if (!obj) return thenDo(null, []); var err, completions; try { var excludes = []; completions = getProtoChain(obj).map(function(proto) { var descr = getDescriptorOf(obj, proto), methodsAndAttributes = getMethodsOf(excludes, proto) .concat(getAttributesOf(excludes, proto)); excludes = excludes.concat(pluck(methodsAndAttributes, 'name')); return [descr, pluck(methodsAndAttributes, 'completion')]; }); } catch (e) { err = e; } thenDo(err, completions); } function getCompletions(evalFunc, string, thenDo) { // thendo = function(err, completions/*ARRAY*/) // eval string and for the resulting object find attributes and methods, // grouped by its prototype / class chain // if string is something like "foo().bar.baz" then treat "baz" as start // letters = filter for properties of foo().bar // ("foo().bar.baz." for props of the result of the complete string) getObjectForCompletion(evalFunc, string, function(err, obj, startLetters) { if (err) { thenDo(err); return } var excludes = []; var completions = getProtoChain(obj).map(function(proto) { var descr = getDescriptorOf(obj, proto), methodsAndAttributes = getMethodsOf(excludes, proto) .concat(getAttributesOf(excludes, proto)); excludes = excludes.concat(pluck(methodsAndAttributes, 'name')); return [descr, pluck(methodsAndAttributes, 'completion')]; }); thenDo(err, completions, startLetters); }) } /* ;(function testCompletion() { function assertCompletions(err, completions, prefix) { assert(!err, 'getCompletions error: ' + err); assert(prefix === '', 'prefix: ' + prefix); assert(completions.length === 3, 'completions does not contain 3 groups ' + completions.length) assert(completions[2][0] === 'Object', 'last completion group is Object') objectCompletions = completions.slice(0,2) expected = [["[object Object]", ["m1(a)","m2(x)","a"]], ["prototype", ["m3(a,b,c)"]]] assert(Objects.equals(expected, objectCompletions), 'compl not equal'); alertOK('all good!') } function evalFunc(string) { return eval(string); } var code = "obj1 = {m2: function() {}, m3:function(a,b,c) {}}\n" + "obj2 = {a: 3, m1: function(a) {}, m2:function(x) {}, __proto__: obj1}\n" + "obj2." getCompletions(evalFunc, code, assertCompletions) })(); */ } }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // ace extensions // -=-=-=-=-=-=-=- AutoComplete.prototype.showPopup = fun.wrap( fun.getOriginal(AutoComplete.prototype.showPopup), function(proceed, editor) { // completion should also work with "backwards" selection: reverse the // selection before cursor movements are interpreted to close the completion // popup var sel = editor.selection; if (sel.isBackwards()) sel.setRange(sel.getRange(), false); return proceed(editor); }); AutoComplete.prototype.openPopup = fun.wrap( fun.getOriginal(AutoComplete.prototype.openPopup), function(proceed, editor, prefix, keepPopupPosition) { proceed(editor, prefix, keepPopupPosition); if (!this.activated) return; setTimeout(function() { // delayed so that we still have the selection when computing the // completions but not for the insertion... if (!editor.selection.isEmpty()) collapseSelection(editor, "end"); }, 20); this.popup.renderer.container.style.zIndex=-1000; this.popup.setFontSize(editor.getFontSize()-3); var parentEditor = editor.parent3d; var popupEditor3d = this.popupEditor3d; if (!popupEditor3d) { popupEditor3d = this.popupEditor3d = new THREE.CodeEditor({ events: parentEditor.events, aceEditor: this.popup }); } var size = this.popup.renderer.$size; popupEditor3d.setSize(size.scrollerWidth, size.scrollerHeight); var bounds = this.popup.renderer.container.getBoundingClientRect(); alignPlanesTopLeft(popupEditor3d, parentEditor, new THREE.Vector3(bounds.left,-bounds.top,.1)); if (!popupEditor3d.parent) parentEditor.parent.add(popupEditor3d); }); AutoComplete.prototype.detach = fun.wrap( fun.getOriginal(AutoComplete.prototype.detach), function(proceed) { this.popupEditor3d && this.popupEditor3d.parent && this.popupEditor3d.parent.remove(this.popupEditor3d); proceed(); }); FilteredList.prototype.filterCompletions = fun.wrap( fun.getOriginal(FilteredList.prototype.filterCompletions), function(proceed, items,needle) { var dynamicCompletions = items.filter(function(ea) { return ea.meta === "dynamic"; }); var result = proceed(items, needle); if (!needle) { // make sure the dynamic completions come first var maxScore = lively.lang.arr.max(result, function(ea) { return ea.score; }).score; if (!result.length) result = dynamicCompletions; dynamicCompletions.forEach(function(ea) { ea.score += maxScore; }); } return result; // var matchedDynamic = result.filter(function(ea) { return ea.meta === "dynamic"; }); // var unmatchedDynamic = lively.lang.arr.withoutAll(dynamicCompletions, matchedDynamic); // console.log("#all / #unmatched: %s/%s", matchedDynamic.length, unmatchedDynamic.length); // return matchedDynamic // .concat(lively.lang.arr.withoutAll(result, matchedDynamic)) // .concat(unmatchedDynamic); }) AutoComplete.prototype.commands = lively.lang.obj.merge(AutoComplete.prototype.commands, { "Alt-Shift-,": function(editor) { editor.completer.goTo("start"); }, "Alt-Shift-.": function(editor) { editor.completer.goTo("end"); }, "Alt-V": function(editor) { editor.completer.popup.gotoPageUp(); }, "Ctrl-V": function(editor) { editor.completer.popup.gotoPageDown(); } }); // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // helper functions // -=-=-=-=-=-=-=-=- function alignPlanesTopLeft(plane1, plane2, offset) { plane1.rotation.copy(plane2.rotation.clone()); plane1.updateMatrixWorld(true); var a = plane1.geometry.vertices[0].clone().applyMatrix4(plane1.matrixWorld); var b = plane2.geometry.vertices[0].clone().applyMatrix4(plane2.matrixWorld); var offsetWorld = offset.applyMatrix4(plane1.matrixWorld).sub(plane1.position); var fromAtoB = b.clone().sub(a).add(offsetWorld); plane1.position.add(fromAtoB); plane1.updateMatrixWorld(true); } // FIXME move to another place function collapseSelection(ed, dir) { // dir = 'start' || 'end' var sel = ed.selection, range = sel.getRange(); dir && sel.moveCursorToPosition(range[dir]); sel.clearSelection(); } })(THREE.CodeEditor.autocomplete || (THREE.CodeEditor.autocomplete = {}));