From ef19fdbcb5af2e8b73fcefe243ba353fcaa00ee7 Mon Sep 17 00:00:00 2001 From: Garen Torikian Date: Mon, 24 Dec 2012 00:21:19 -0800 Subject: [PATCH] First pass at Haurtyun UI + C9 doc parsing --- lib/ace/autocomplete.js | 75 +++++---- lib/ace/autocomplete/autocomplete_worker.js | 128 ++++++++++++++ lib/ace/autocomplete/complete_util.js | 85 ++++++++++ lib/ace/autocomplete/syntax_detector.js | 176 ++++++++++++++++++++ lib/ace/autocomplete/text_completer.js | 74 ++++++++ lib/ace/worker/mirror.js | 6 +- lib/ace/worker/worker_client.js | 6 +- 7 files changed, 511 insertions(+), 39 deletions(-) create mode 100644 lib/ace/autocomplete/autocomplete_worker.js create mode 100644 lib/ace/autocomplete/complete_util.js create mode 100644 lib/ace/autocomplete/syntax_detector.js create mode 100644 lib/ace/autocomplete/text_completer.js diff --git a/lib/ace/autocomplete.js b/lib/ace/autocomplete.js index 4c751d86..df71dcc2 100644 --- a/lib/ace/autocomplete.js +++ b/lib/ace/autocomplete.js @@ -38,6 +38,7 @@ var UndoManager = require("ace/undomanager").UndoManager; var dom = require("ace/lib/dom"); var HashHandler = require("ace/keyboard/hash_handler").HashHandler; var TextMode = require("ace/mode/text").Mode; +var WorkerClient = require("./worker/worker_client").WorkerClient; var mode = new TextMode(); mode.$tokenizer = { @@ -45,6 +46,8 @@ mode.$tokenizer = { } }; +var worker = new WorkerClient(["ace"], "ace/autocomplete/autocomplete_worker", "AutocompleteWorker"); + var Autocomplete = function() { this.keyboardHandler = new HashHandler(); this.keyboardHandler.bindKeys(this.commands); @@ -68,28 +71,28 @@ var Autocomplete = function() { el.style.display = "none" popup.renderer.content.style.cursor="default" - var nop = function(){}; + var noop = function(){}; - popup.focus = nop; + popup.focus = noop; popup.$isFocused = true; - popup.renderer.$cursorLayer.restartTimer = nop - popup.renderer.$cursorLayer.update = nop + popup.renderer.$cursorLayer.restartTimer = noop; + popup.renderer.$cursorLayer.update = noop; popup.renderer.$cursorLayer.element.style.display = "none"; popup.renderer.maxLines = 6 popup.renderer.$keepTextAreaAtCursor=false - popup.setHighlightActiveLine(true) - popup.setSession(new EditSession("")) + popup.setHighlightActiveLine(true); + popup.setSession(new EditSession("")); popup.on("mousedown", function(e) { var pos = e.getDocumentPosition(); popup.moveCursorToPosition(pos); popup.selection.clearSelection(); e.stop(); - }) + }); /* popup.session.setMode({ $ @@ -158,20 +161,6 @@ var Autocomplete = function() { el.style.display = ""; }; - this.attachToEditor = function(editor) { - if (this.editor) - this.detach(); - this.editor = editor; - if (editor.Autocomplete != this) { - if (editor.Autocomplete) - editor.Autocomplete.detach(); - editor.Autocomplete = this; - } - editor.keyBinding.addKeyboardHandler(this.keyboardHandler) - editor.on("changeSelection", this.$changeListener) - editor.on("blur", this.$blurListener) - }; - this.detach = function() { this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler); this.editor.removeEventListener("changeSelection", this.changeListener); @@ -232,20 +221,38 @@ var Autocomplete = function() { }; this.complete = function(editor) { - this.attachToEditor(editor); - var data = this.gatherCompletions(editor); - this.completions = new FilteredList(data); - this.completions.setFilter("a") - if (data) { - if (data.length == 1) - this.insertMatch(0); - else - this.openPopup(editor); + if (this.editor) + this.detach(); + + var _self = this; + this.editor = editor; + if (editor.Autocomplete != this) { + if (editor.Autocomplete) + editor.Autocomplete.detach(); + editor.Autocomplete = this; } - }; - - this.gatherCompletions = function() { - return ["asdaf", "foo", "bar", "baz"]; + + editor.keyBinding.addKeyboardHandler(this.keyboardHandler); + editor.on("changeSelection", this.$changeListener); + editor.on("blur", this.$blurListener); + + worker.attachToDocument(editor.session.getDocument(), {cursor: editor.getCursorPosition()}); + + worker.on("complete", function(data) { + _self.completions = new FilteredList(data.data.matches); + _self.completions.setFilter("a"); + + if (data) { + if (data.length == 1) + _self.insertMatch(0); + else + _self.openPopup(editor); + } + }); + + worker.on("terminate", function() { + console.log("term"); + }); }; this.$singleLineEditor = function(el) { diff --git a/lib/ace/autocomplete/autocomplete_worker.js b/lib/ace/autocomplete/autocomplete_worker.js new file mode 100644 index 00000000..2656b086 --- /dev/null +++ b/lib/ace/autocomplete/autocomplete_worker.js @@ -0,0 +1,128 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Distributed under the BSD license: + * + * Copyright (c) 2010, Ajax.org B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Ajax.org B.V. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var Mirror = require("../worker/mirror").Mirror; + +var SyntaxDetector = require("./syntax_detector"); +var completer = require("./text_completer"); + +var AutocompleteWorker = exports.AutocompleteWorker = function(sender) { + Mirror.call(this, sender); +}; + +oop.inherits(AutocompleteWorker, Mirror); + +(function() { + // For code completion + function removeDuplicateMatches(matches) { + // First sort + matches.sort(function(a, b) { + if (a.name < b.name) + return 1; + else if (a.name > b.name) + return -1; + else + return 0; + }); + for (var i = 0; i < matches.length - 1; i++) { + var a = matches[i]; + var b = matches[i + 1]; + if (a.name === b.name) { + // Duplicate! + if (a.priority < b.priority) + matches.splice(i, 1); + else if (a.priority > b.priority) + matches.splice(i+1, 1); + else if (a.score < b.score) + matches.splice(i, 1); + else if (a.score > b.score) + matches.splice(i+1, 1); + else + matches.splice(i, 1); + i--; + } + } + }; + + this.onUpdate = function() { + var _self = this; + + var doc = this.doc.getValue(); + var pos = this.data.cursor; + var part = SyntaxDetector.getContextSyntaxPart(this.doc, this.data.cursor, "javascript"); + var language = part.language; + + var currentPos = { line: pos.row, col: pos.column }; + var currentNode = null; + var matches = [], ast = null; + + completer.complete(_self.doc, ast, this.data.cursor, currentNode, function(completions) { + if (completions) + matches = matches.concat(completions); + removeDuplicateMatches(matches); + // Sort by priority, score + matches.sort(function(a, b) { + if (a.priority < b.priority) + return 1; + else if (a.priority > b.priority) + return -1; + else if (a.score < b.score) + return 1; + else if (a.score > b.score) + return -1; + else if (a.id && a.id === b.id) { + if (a.isFunction) + return -1; + else if (b.isFunction) + return 1; + } + if (a.name < b.name) + return -1; + else if(a.name > b.name) + return 1; + else + return 0; + }); + _self.sender.emit("complete", { + pos: pos, + matches: matches, + line: _self.doc.getLine(pos.row) + }); + return; + }); + }; + +}).call(AutocompleteWorker.prototype); + +}); \ No newline at end of file diff --git a/lib/ace/autocomplete/complete_util.js b/lib/ace/autocomplete/complete_util.js new file mode 100644 index 00000000..4b7e6a16 --- /dev/null +++ b/lib/ace/autocomplete/complete_util.js @@ -0,0 +1,85 @@ +define(function(require, exports, module) { + +var ID_REGEX = /[a-zA-Z_0-9\$]/; + +function retrievePrecedingIdentifier(text, pos, regex) { + regex = regex || ID_REGEX; + var buf = []; + for (var i = pos-1; i >= 0; i--) { + if (regex.test(text[i])) + buf.push(text[i]); + else + break; + } + return buf.reverse().join(""); +} + +function retrieveFollowingIdentifier(text, pos, regex) { + regex = regex || ID_REGEX; + var buf = []; + for (var i = pos; i < text.length; i++) { + if (regex.test(text[i])) + buf.push(text[i]); + else + break; + } + return buf; +} + +function prefixBinarySearch(items, prefix) { + var startIndex = 0; + var stopIndex = items.length - 1; + var middle = Math.floor((stopIndex + startIndex) / 2); + + while (stopIndex > startIndex && middle >= 0 && items[middle].indexOf(prefix) !== 0) { + if (prefix < items[middle]) { + stopIndex = middle - 1; + } + else if (prefix > items[middle]) { + startIndex = middle + 1; + } + middle = Math.floor((stopIndex + stopIndex) / 2); + } + + // Look back to make sure we haven't skipped any + while (middle > 0 && items[middle-1].indexOf(prefix) === 0) + middle--; + return middle >= 0 ? middle : 0; // ensure we're not returning a negative index +} + +function findCompletions(prefix, allIdentifiers) { + allIdentifiers.sort(); + var startIdx = prefixBinarySearch(allIdentifiers, prefix); + var matches = []; + for (var i = startIdx; i < allIdentifiers.length && allIdentifiers[i].indexOf(prefix) === 0; i++) + matches.push(allIdentifiers[i]); + return matches; +} + +function fetchText(staticPrefix, path) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', staticPrefix + "/" + path, false); + try { + xhr.send(); + } + // Likely we got a cross-script error (equivalent with a 404 in our cloud setup) + catch(e) { + return false; + } + if (xhr.status === 200) + return xhr.responseText; + else + return false; +} + +/** @deprecated Use retrievePrecedingIdentifier */ +exports.retrievePreceedingIdentifier = function() { + console.error("Deprecated: 'retrievePreceedingIdentifier' - use 'retrievePrecedingIdentifier' instead"); + return retrievePrecedingIdentifier.apply(null, arguments); +}; +exports.retrievePrecedingIdentifier = retrievePrecedingIdentifier; +exports.retrieveFollowingIdentifier = retrieveFollowingIdentifier; +exports.findCompletions = findCompletions; +exports.fetchText = fetchText; + +}); diff --git a/lib/ace/autocomplete/syntax_detector.js b/lib/ace/autocomplete/syntax_detector.js new file mode 100644 index 00000000..40cfd3f1 --- /dev/null +++ b/lib/ace/autocomplete/syntax_detector.js @@ -0,0 +1,176 @@ +/** + * Cloud9 Language Foundation + * + * @copyright 2011, Ajax.org B.V. + * @license GPLv3 + */ +define(function(require, exports, module) { + +var mixedLanguages = { + php: { + "default": "html", + "php-start": /<\?(?:php|\=)?/, + "php-end": /\?>/, + "css-start": /]*>/, + "css-end": /<\/style>/, + "javascript-start": /\/])*>/, + "javascript-end": /<\/script>/ + }, + html: { + "css-start": /]*>/, + "css-end": /<\/style>/, + "javascript-start": /\/])*>/, + "javascript-end": /<\/script>/ + } +}; + +/* Now: + * - One level syntax nesting supported + * Future: (if worth it) + * - Have a stack to repesent it + * - Maintain a syntax tree for an opened file + */ +function getSyntaxRegions(doc, originalSyntax) { + if (! mixedLanguages[originalSyntax]) + return [{ + syntax: originalSyntax, + sl: 0, + sc: 0, + el: doc.getLength()-1, + ec: doc.getLine(doc.getLength()-1).length + }]; + + var lines = doc.getAllLines(); + var type = mixedLanguages[originalSyntax]; + var defaultSyntax = type["default"] || originalSyntax; + var starters = Object.keys(type).filter(function (m) { + return m.indexOf("-start") === m.length - 6; + }); + var syntax = defaultSyntax; + var regions = [{syntax: syntax, sl: 0, sc: 0}]; + var starter, endLang; + var tempS, tempM; + var i, m, cut, inLine = 0; + + for (var row = 0; row < lines.length; row++) { + var line = lines[row]; + m = null; + if (endLang) { + m = endLang.exec(line); + if (m) { + endLang = null; + syntax = defaultSyntax; + regions[regions.length-1].el = row; + regions[regions.length-1].ec = m.index + inLine; + regions.push({ + syntax: syntax, + sl: row, + sc: m.index + inLine + }); + cut = m.index + m[0].length; + lines[row] = line.substring(cut); + inLine += cut; + row--; // continue processing of the line + } + else { + inLine = 0; + } + } + else { + for (i = 0; i < starters.length; i++) { + tempS = starters[i]; + tempM = type[tempS].exec(line); + if (tempM && (!m || m.index > tempM.index)) { + m = tempM; + starter = tempS; + } + } + if (m) { + syntax = starter.replace("-start", ""); + endLang = type[syntax+"-end"]; + regions[regions.length-1].el = row; + regions[regions.length-1].ec = inLine + m.index + m[0].length; + regions.push({ + syntax: syntax, + sl: row, + sc: inLine + m.index + m[0].length + }); + cut = m.index + m[0].length; + lines[row] = line.substring(m.index + m[0].length); + row--; // continue processing of the line + inLine += cut; + } + else { + inLine = 0; + } + } + } + regions[regions.length-1].el = lines.length; + regions[regions.length-1].ec = lines[lines.length-1].length; + return regions; +} + +function getContextSyntaxPart(doc, pos, originalSyntax) { + if (! mixedLanguages[originalSyntax]) + return { + language: originalSyntax, + value: doc.getValue(), + region: getSyntaxRegions(doc, originalSyntax)[0], + index: 0 + }; + var regions = getSyntaxRegions(doc, originalSyntax); + for (var i = 0; i < regions.length; i++) { + var region = regions[i]; + if ((pos.row > region.sl && pos.row < region.el) || + (pos.row === region.sl && pos.column >= region.sc) || + (pos.row === region.el && pos.column <= region.ec)) + return regionToCodePart(doc, region, i); + } + return null; // should never happen +} + +function getContextSyntax(doc, pos, originalSyntax) { + var part = getContextSyntaxPart(doc, pos, originalSyntax); + return part && part.language; // should never happen +} + +function regionToCodePart (doc, region, index) { + var lines = doc.getLines(region.sl, region.el); + return { + value: region.sl === region.el ? lines[0].substring(region.sc, region.ec) : + [lines[0].substring(region.sc)].concat(lines.slice(1, lines.length-1)).concat([lines[lines.length-1].substring(0, region.ec)]).join(doc.getNewLineCharacter()), + language: region.syntax, + region: region, + index: index + }; +} + +function getCodeParts (doc, originalSyntax) { + var regions = getSyntaxRegions(doc, originalSyntax); + return regions.map(function (region, i) { + return regionToCodePart(doc, region, i); + }); +} + +function posToRegion (region, pos) { + return { + row: pos.row - region.sl, + column: pos.column + }; +} + +function regionToPos (region, pos) { + return { + row: pos.row + region.sl, + column: pos.column + }; +} + +exports.getContextSyntax = getContextSyntax; +exports.getContextSyntaxPart = getContextSyntaxPart; +exports.getSyntaxRegions = getSyntaxRegions; +exports.getCodeParts = getCodeParts; +exports.posToRegion = posToRegion; +exports.regionToPos = regionToPos; + +}); diff --git a/lib/ace/autocomplete/text_completer.js b/lib/ace/autocomplete/text_completer.js new file mode 100644 index 00000000..d98ec95f --- /dev/null +++ b/lib/ace/autocomplete/text_completer.js @@ -0,0 +1,74 @@ +define(function(require, exports, module) { + var completeUtil = require("./complete_util"); + var SPLIT_REGEX = /[^a-zA-Z_0-9\$]+/; + + var completer = module.exports; + + this.handlesLanguage = function(language) { + return true; + }; + + // For the current document, gives scores to identifiers not on frequency, but on distance from the current prefix + function wordDistanceAnalyzer(doc, pos, prefix) { + var text = doc.getValue().trim(); + + // Determine cursor's word index + var textBefore = doc.getLines(0, pos.row-1).join("\n") + "\n"; + var currentLine = doc.getLine(pos.row); + textBefore += currentLine.substr(0, pos.column); + var prefixPosition = textBefore.trim().split(SPLIT_REGEX).length - 1; + + // Split entire document into words + var identifiers = text.split(SPLIT_REGEX); + var identDict = {}; + + // Find prefix to find other identifiers close it + for (var i = 0; i < identifiers.length; i++) { + if (i === prefixPosition) + continue; + var ident = identifiers[i]; + if (ident.length === 0) + continue; + var distance = Math.max(prefixPosition, i) - Math.min(prefixPosition, i); + // Score substracted from 100000 to force descending ordering + if (Object.prototype.hasOwnProperty.call(identDict, ident)) + identDict[ident] = Math.max(1000000-distance, identDict[ident]); + else + identDict[ident] = 1000000-distance; + + } + return identDict; + } + + function analyze(doc, pos) { + var line = doc.getLine(pos.row); + var identifier = completeUtil.retrievePrecedingIdentifier(line, pos.column); + + var analysisCache = wordDistanceAnalyzer(doc, pos, identifier); + return analysisCache; + } + + completer.complete = function(doc, fullAst, pos, currentNode, callback) { + var identDict = analyze(doc, pos); + var line = doc.getLine(pos.row); + var identifier = completeUtil.retrievePrecedingIdentifier(line, pos.column); + + var allIdentifiers = []; + for (var ident in identDict) { + allIdentifiers.push(ident); + } + var matches = completeUtil.findCompletions(identifier, allIdentifiers); + + callback(matches); + /*callback(matches.map(function(m) { + return { + name : m, + replaceText : m, + icon : null, + score : identDict[m], + meta : "", + priority : 1 + }; + }));*/ + }; +}); \ No newline at end of file diff --git a/lib/ace/worker/mirror.js b/lib/ace/worker/mirror.js index c521f8fd..47412452 100644 --- a/lib/ace/worker/mirror.js +++ b/lib/ace/worker/mirror.js @@ -7,7 +7,8 @@ var lang = require("../lib/lang"); var Mirror = exports.Mirror = function(sender) { this.sender = sender; var doc = this.doc = new Document(""); - + this.data = {}; + var deferredUpdate = this.deferredUpdate = lang.delayedCall(this.onUpdate.bind(this)); var _self = this; @@ -25,8 +26,9 @@ var Mirror = exports.Mirror = function(sender) { this.$timeout = timeout; }; - this.setValue = function(value) { + this.setValue = function(value, data) { this.doc.setValue(value); + this.data = data; this.deferredUpdate.schedule(this.$timeout); }; diff --git a/lib/ace/worker/worker_client.js b/lib/ace/worker/worker_client.js index 5a682fe5..f48599c4 100644 --- a/lib/ace/worker/worker_client.js +++ b/lib/ace/worker/worker_client.js @@ -144,12 +144,12 @@ var WorkerClient = function(topLevelNamespaces, mod, classname) { catch(ex) {} }; - this.attachToDocument = function(doc) { - if(this.$doc) + this.attachToDocument = function(doc, data) { + if (this.$doc) this.terminate(); this.$doc = doc; - this.call("setValue", [doc.getValue()]); + this.call("setValue", [doc.getValue(), data]); doc.on("change", this.changeListener); };