brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From henev...@apache.org
Subject [16/27] brooklyn-ui git commit: simple but effective code completion
Date Thu, 18 Feb 2016 10:37:08 GMT
simple but effective code completion

does not attempt to cover everything or do a perfect parse tree,
as that is much harder, but does simple heuristics which give a lot of help without too much
work

it would be great to offer something much sounder, basing e.g. on json-schema,
but that is a lot more work as well


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/commit/1058fa7a
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/tree/1058fa7a
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/diff/1058fa7a

Branch: refs/heads/master
Commit: 1058fa7a1328bb1926dc1f0f68e580dc323dd588
Parents: d19e315
Author: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Authored: Fri Feb 12 15:55:30 2016 +0000
Committer: Alex Heneveld <alex.heneveld@cloudsoftcorp.com>
Committed: Tue Feb 16 02:08:31 2016 +0000

----------------------------------------------------------------------
 .../webapp/assets/css/codemirror-brooklyn.css   |  14 +
 src/main/webapp/assets/js/config.js             |   2 +
 .../brooklyn-yaml-completion-proposals.js       | 374 +++++++++++++++++++
 .../js/util/code-complete/js-yaml-parser.js     |  48 +++
 src/main/webapp/assets/js/view/editor.js        |  25 +-
 src/test/javascript/config.txt                  |   2 +
 6 files changed, 463 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/main/webapp/assets/css/codemirror-brooklyn.css
----------------------------------------------------------------------
diff --git a/src/main/webapp/assets/css/codemirror-brooklyn.css b/src/main/webapp/assets/css/codemirror-brooklyn.css
index c293ed4..b1b2c54 100644
--- a/src/main/webapp/assets/css/codemirror-brooklyn.css
+++ b/src/main/webapp/assets/css/codemirror-brooklyn.css
@@ -27,3 +27,17 @@
 }
 .CodeMirror .CodeMirror-gutter.CodeMirror-linenumbers {
 }
+
+li.CodeMirror-hint {
+  max-width: inherit;
+  overflow: visible;
+}
+ul.CodeMirror-hints {
+  overflow: scroll;
+  max-width: 30em;
+}
+
+.CodeMirror-hints .CodeMirror-hint.summary {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-style: italic;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/main/webapp/assets/js/config.js
----------------------------------------------------------------------
diff --git a/src/main/webapp/assets/js/config.js b/src/main/webapp/assets/js/config.js
index 05afbb6..94d6385 100644
--- a/src/main/webapp/assets/js/config.js
+++ b/src/main/webapp/assets/js/config.js
@@ -49,6 +49,8 @@ require.config({
         "uri":"libs/URI",
         "zeroclipboard":"libs/ZeroClipboard",
         "js-yaml":"libs/js-yaml",
+        "js-yaml-parser":"util/code-complete/js-yaml-parser",
+        "brooklyn-yaml-completion-proposals":"util/code-complete/brooklyn-yaml-completion-proposals",
 
         "codemirror":"libs/codemirror",
         "codemirror-mode-yaml":"libs/codemirror/mode/yaml/yaml",

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/main/webapp/assets/js/util/code-complete/brooklyn-yaml-completion-proposals.js
----------------------------------------------------------------------
diff --git a/src/main/webapp/assets/js/util/code-complete/brooklyn-yaml-completion-proposals.js
b/src/main/webapp/assets/js/util/code-complete/brooklyn-yaml-completion-proposals.js
new file mode 100644
index 0000000..032f0d6
--- /dev/null
+++ b/src/main/webapp/assets/js/util/code-complete/brooklyn-yaml-completion-proposals.js
@@ -0,0 +1,374 @@
+
+define([
+    "backbone", "js-yaml-parser", "underscore"
+], function (Backbone, JsYamlParser, _) {
+
+    var BrooklynYamlCompletionProposals = {};
+
+    var Catalog = Backbone.Collection.extend({
+        initialize: function(models, options) {
+            this.name = options["name"];
+            var that = this; 
+            var model = this.model.extend({
+              url: function() {
+                return "/v1/" + that.name + "/" + this.id.split(":").join("/");
+              }
+            });
+            _.bindAll(this);
+            this.model = model;
+        },
+        url: function() {
+            return "/v1/" + this.name;
+        }
+    });
+    // currently populates catalog on startup, then refreshes on each search
+    // (but only renders later)
+    // ideally should be shared model refreshed periodically in background
+    var catalogE = new Catalog(undefined, { name: "catalog/entities" });
+    var catalogA = new Catalog(undefined, { name: "catalog/applications" });
+    var catalogL = new Catalog(undefined, { name: "locations" });
+    catalogA.fetch();
+    catalogE.fetch();
+    catalogL.fetch();
+
+    function findParseNodeAt(parse, position) {
+      if (parse.start<=position && parse.end>=position) {
+        for (var ci in parse.children) {
+          var c = parse.children[ci];
+          var result = findParseNodeAt(c, position);
+          if (result) return result;
+        }
+        return parse;
+      }
+      return null;
+    }
+    
+    /** adds additional fields to a parse node:
+     *  * type: map, list, primitive, null
+     *  * role: key, value, entry, root; or primitive if we are a primitive in a primitive
(due to how it is parsed) 
+     *  * key: if we are role 'value', what is our key
+     *  * index: if parent is map or list, what is our position in that as a list
+     *  and returns a list of the containing parse nodes, root first
+     */
+    function findContexts(n, position) {
+        if (!n || n.role) return;
+        
+        if (n.result === null) {
+            n.type = 'null';
+        } else if (typeof n.result === 'object') {
+            if (typeof n.result.length !== 'undefined') {
+                // ^^^ messy way to check is parent a list
+                n.type = 'list';
+            } else {
+                n.type = 'map';
+            }
+        } else {
+            n.type = 'primitive';
+        }
+        
+        if (!n.parent) {
+            n.role = 'root';
+            n.depth = 0;
+            return [n];
+        }
+        var result = findContexts(n.parent, position);
+        n.depth = n.parent.depth+1;
+        result.push(n);
+        
+        if (n.parent.type == 'map') {
+            var prev;            
+            for (var ci in n.parent.children) {
+                var c = n.parent.children[ci];
+                if (c === n || c.start > position) {
+                    n.role = (ci%2==0 ? 'key' : 'value');
+                    n.index = (ci - (ci%2))/2;
+                    if (n.role === 'value') {
+                      n.key = prev;
+                    }
+                    return result;
+                }
+                prev = c;
+            }
+            console.log("not found",n,"in",n.parent.children);
+            throw "did not find parse node in parent's children";
+        }
+        
+        if (n.parent.type == 'list') {
+            n.role = 'entry';
+            for (var ci in n.children) {
+                var c = n.parent.children[ci];
+                if (c === n || c.start > position) {
+                    n.index = c;
+                    return result;
+                }
+            }
+            console.log("not found",n,"in",n.parent.children);
+            throw "did not find parse node in parent's children";
+        }
+        
+        n.role = 'primitive';
+        return result;
+    }
+    
+    function findContainingParseNode(n, predicate) {
+        if (typeof n === 'undefined') return null;
+        if (predicate(n)) return n;
+        return findContainingParseNode(n.parent, predicate);
+    }
+    
+    function findContainingMapParseNode(n) {
+        return findContainingParseNode(n, function() { return n.type === 'map'; });
+    }
+    
+    function indentation(n) {
+      var i = n.start;
+      while (i>0 && n.doc.charAt(i-1)!='\n') i--;
+      return n.start - i;
+    }
+
+    function spaces(n) {
+      var result = '';
+      while (n>0) { result+=' '; n--; }
+      return result;
+    }
+        
+    var CatalogProposer = {
+        getRootProposals: function() {
+          return [
+              { displayText: "brooklyn.catalog:", text: "brooklyn.catalog:\n  items:\n  -
" }
+            ];
+        },
+        getProposals: function(nn, position, cmPosition) {
+            var n = nn[nn.length-1];
+            var result = [];
+            
+            var itemsKey;
+            if (n.type=='list' && n.key.result=='items') itemsKey = n.key;
+            else if (n.type='entry' && nn.length>2 && nn[nn.length-2].type=='list'
&& nn[nn.length-2].key.result=='items') itemsKey = nn[nn.length-2].key;
+            if (itemsKey) {            
+                result = result.concat(_.map(["id","name","itemType"],
+                    function(s) { return { displayText: s, text: spaces(indentation(itemsKey)+2)+s+':
' } }));
+                result.push({displayText: "item", text: spaces(indentation(itemsKey)+2)+"item"+':\n'+spaces(indentation(n)+2)
});
+                result = result.concat(_.map(["description","iconUrl"],
+                    function(s) { return { displayText: s, text: spaces(indentation(itemsKey)+2)+s+':
' } }));
+                result.push({displayText: "items", text: spaces(indentation(itemsKey)+2)+"items"+':\n'+spaces(indentation(n))+"-
" });
+            }
+
+            if (n.key && n.key.result=='itemType') {
+              result.concat(['template','entity','location','policy']);
+            }
+            
+            if (n.depth==1 || (n.depth>1 && cmPosition.ch==0)) {
+                result.concat([{displayText: "version", text: "  version:\n"}, 
+                    {displayText: "items", text: "  items:\n  - \n"} ]);
+            }
+            
+            return result;
+        }
+    }
+    
+    var AppBlueprintProposer = {
+        getRootProposals: function() {
+          return [
+              { displayText: "name:", text: "name: " },
+              { displayText: "location:", text: "location:\n  " },
+              { displayText: "services:", text: "services:\n- type: " }
+            ];
+        },
+        
+        getServiceTypes: function(n) {
+            var result = [];
+            console.log(catalogE);
+            catalogE.fetch();
+            catalogA.fetch();
+            result = result.concat(_.map(catalogE.models, function(m) { return m.get('symbolicName');
})); 
+            result = result.concat(_.map(catalogA.models, function(m) { return m.get('symbolicName');
}));
+            return result; 
+        },
+        getServiceKeys: function(type) {
+            t = catalogA.get(type) || catalogE.get(type) ||
+                // look for type without ID
+                _.find(catalogA.models, function(m) { return m.get('symbolicName') == type;
}) || 
+                _.find(catalogE.models, function(m) { return m.get('symbolicName') == type;
}); 
+            if (!t) return [];
+            return _.map(t.get('config'), function(c) { return c.name; });
+        },
+        getServiceKeyProposals: function(type, key) {
+            t = catalogA.get(type) || catalogE.get(type) ||
+                // look for type without ID
+                _.find(catalogA.models, function(m) { return m.get('symbolicName') == type;
}) || 
+                _.find(catalogE.models, function(m) { return m.get('symbolicName') == type;
}); 
+            if (!t) return [];
+            var c = _.find(t.get('config'), function(c) { return c.name == key; });
+            if (!c) return [];
+            var ct = c.type;
+            if (ct.startsWith("java.lang.") || ct.startsWith("java.util.")) ct = ct.substring(10);
+            var result = [ { displayText: ct+": "+c.description, text: '', className: 'summary'
} ]; 
+            if (c.possibleValues) {
+                _.each(c.possibleValues, function(v) { result.push(v.value); });
+            }
+            return result;
+        },
+    
+        getLocationTypes: function() {
+            catalogL.fetch();
+            return _.map(catalogL.models, function(m) { return m.get('name'); });
+        },    
+        
+        // TODO would be much nicer to define a yaml schema; see e.g. json-schema.org
+        // and various JS implementations
+        getProposals: function(nn, position, cmPosition) {
+            var n = nn[nn.length-1];
+            var result = [];
+//            console.log("context at position "+position, nn);
+            while (n.role === 'primitive') n = n.parent;
+            if (nn[1].key && nn[1].key.result === 'services') {
+                // in services block
+                var canAddService = true;
+                
+                if (n.depth == 3 && n.role == 'value' && n.parent.role ==
'entry') {
+                    // in a block for a particular service
+                    canAddService = false;
+                    if (n.key.result === 'type') {
+                        result = result.concat(_.map(this.getServiceTypes(n.parent), function(t)
{ return t+'\n'; }));
+                    } else if (n.key.result === 'location') {
+                        result = result.concat(_.map(this.getLocationTypes(n.parent), function(t)
{ 
+                            return t+'\n'; }));
+                    
+                    } else {
+                        // no assistance for values of other keys atm; show summary if available
+                        var type = nn[2].result['type'];
+                        result = result.concat(
+                            this.getServiceKeyProposals(type, n.key.result) ||
+                                [{ displayText: 'No assistance available for key', 
+                                   className: 'summary', text: '' }]);
+                    }
+                }
+                
+                if (n.depth >= 2 && n.role == 'entry' && nn[2].result['type'])
{
+                    var type = nn[2].result['type'];
+                    result = result.concat(
+                        _.map(this.getServiceKeys(type), function(keyname) {
+                            return { displayText: keyname, text: spaces(indentation(nn[2]))+keyname+":
" };
+                        }));
+                }
+                
+                if (n.depth > 2) {
+                    // deep in a service, no special assistance currently offered
+                }
+                
+                if (canAddService && (position == nn[1].end || position == nn[1].end+1))
{
+                    result.push( { displayText: 'Add a service', className: 'summary', text:
'\n- type: ' } );
+                }
+            }
+            if (nn[1].key && nn[1].key.result === 'location') {
+                if (n.depth <= 2) {
+                    result = result.concat(_.map(this.getLocationTypes(n.parent), function(t)
{ 
+                            return t+'\n'; }));
+                }
+            }
+            // TODO other blocks
+            return result;
+        }
+    }
+
+    // cmPosition is {line: N, ch: N} format
+    BrooklynYamlCompletionProposals.getCompletionProposals = function(mode, cm) {
+        var proposer = mode === 'catalog' ? CatalogProposer : AppBlueprintProposer;
+        var text = cm.getValue();
+        var cmPosition = cm.getCursor();
+        // absolute position in doc
+        var position = cm.getRange({line: 0, ch: 0}, cmPosition).length;
+         
+        var parse;
+        try {
+            parse = JsYamlParser.parse(text);
+            if (typeof parse.result == 'string') {
+                console.log("prim");
+                throw "primitive not supported, parse as empty and let completion apply";
+            }
+        } catch (e) {
+            // parse failed -- parse to beginning of line
+            try {
+                parse = JsYamlParser.parse(text.substring(0, text.length - cmPosition.ch)+spaces(cmPosition.ch));
+            } catch (e) {
+                console.log('parse failed', e);
+                return [];
+            }
+        }
+        try {
+            var result;
+            if (typeof parse === 'undefined') {
+                // editor empty -- return defaults
+                result = proposer.getRootProposals();
+            } else {
+                n = findParseNodeAt(parse, position);
+                if (!n) {
+                  // shouldn't happen... fall back to returning empty
+                  console.log("no parse node containing curpos");
+                  result = proposer.getRootProposals();
+                } else {
+                    var nn = findContexts(n, position);
+                    
+                    if (n.role === 'root') {
+                      result = proposer.getRootProposals();
+                    } else {
+                      result = proposer.getProposals(nn, position, cmPosition);
+                    }
+                }
+            }
+            var wordSoFar = '';
+            var lineSoFar = '';
+            var i=0;
+            while (position > wordSoFar.length) {
+                var c = text.charAt(position-wordSoFar.length-1);
+                if (c=='\n' || c==' ' || c=='\t') break;
+                wordSoFar = '' + c + wordSoFar;
+            }
+            while (position > lineSoFar.length) {
+                var c = text.charAt(position-lineSoFar.length-1);
+                if (c=='\n') break;
+                lineSoFar = '' + c + lineSoFar;
+            }
+            result = _.compact(_.map(result, function(proposal) {
+                var proposalObj;
+                console.log("considering",proposal,"wrt",wordSoFar,"/",lineSoFar);
+                if (typeof proposal === 'object') {
+                    proposalObj = proposal;
+                    proposal = proposalObj.text;
+                }  else {
+                    proposalObj = { text: proposal, displayText: proposal };
+                }
+                if (proposal[0]==' ') {
+                  // proposal should start with 'lineSoFar'
+                  if (proposal.lastIndexOf(lineSoFar, 0)!=0) {
+                    // also match '  - ' in lieu of '    ',
+                    // needed for catalog (for service we always add the 'type' in the previous
expansion)
+                    var proposalIfListStart = proposal.replace(/( *)  /,'$1- ');
+                    if (proposalIfListStart.lastIndexOf(lineSoFar, 0)!=0) {
+                      return null;
+                    }
+                    proposalObj.text = proposalIfListStart;
+                  }
+                  proposalObj.to = cmPosition;
+                  proposalObj.from = { line: cmPosition.line, ch: 0 };
+                } else {
+                  // proposal should start with 'wordSoFar'
+                  if (proposal.lastIndexOf(wordSoFar, 0)!=0) return null;
+                  proposalObj.to = cmPosition;
+                  proposalObj.from = { line: cmPosition.line, ch: cmPosition.ch - wordSoFar.length
};
+                }
+                return proposalObj;
+            }));
+//            console.log("proposals:", result);
+            return result;
+            
+        } catch (e) {
+            console.log('completion failed', e, e.stack);
+            return [];
+        }
+    }
+    
+    return BrooklynYamlCompletionProposals;
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/main/webapp/assets/js/util/code-complete/js-yaml-parser.js
----------------------------------------------------------------------
diff --git a/src/main/webapp/assets/js/util/code-complete/js-yaml-parser.js b/src/main/webapp/assets/js/util/code-complete/js-yaml-parser.js
new file mode 100644
index 0000000..98092f0
--- /dev/null
+++ b/src/main/webapp/assets/js/util/code-complete/js-yaml-parser.js
@@ -0,0 +1,48 @@
+
+define([
+    "js-yaml"
+], function (jsyaml) {
+
+    'use strict';
+
+    var JsYamlParser = {};
+
+    function newYamlParseNode(doc, parent, state) {
+        return {
+            doc: doc,
+            parent: parent,
+            children: [],
+
+            start: state.position,
+        };
+    }
+
+    function closeYamlNode(node, state) {
+        node.end = state.position;
+        node.result = state.result;
+    }
+
+    /** returns a YamlParseNode containing { doc, parent, children, start, end, result }
*/ 
+    JsYamlParser.parse = function(input) {
+        var rootNode, node;
+        var l = function(event, state) {
+            if (event === 'open') {
+                node = newYamlParseNode(input, node, state);
+            } else if (event == 'close') {
+                closeYamlNode(node, state);
+                if (node.parent) {
+                    node.parent.children.push(node);
+                    node = node.parent;
+                } else {
+                    if (rootNode != null) throw 'doc should have only one root node';
+                    rootNode = node;
+                    node = null;
+                }
+            }
+        };
+        var result = jsyaml.safeLoad(input, {listener: l});
+        return rootNode;
+    };
+
+    return JsYamlParser;
+});

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/main/webapp/assets/js/view/editor.js
----------------------------------------------------------------------
diff --git a/src/main/webapp/assets/js/view/editor.js b/src/main/webapp/assets/js/view/editor.js
index d867080..19e4378 100644
--- a/src/main/webapp/assets/js/view/editor.js
+++ b/src/main/webapp/assets/js/view/editor.js
@@ -17,7 +17,7 @@
  * under the License.
 */
 define([
-    "underscore", "jquery", "backbone", "model/catalog-application", "js-yaml", "codemirror",
+    "underscore", "jquery", "backbone", "model/catalog-application", "js-yaml", "brooklyn-yaml-completion-proposals",
"codemirror",
     "text!tpl/editor/page.html",
 
     // no constructor
@@ -26,7 +26,7 @@ define([
     "jquery-ba-bbq",
     "handlebars",
     "bootstrap"
-], function (_, $, Backbone, CatalogApplication, jsYaml, CodeMirror, EditorHtml) {
+], function (_, $, Backbone, CatalogApplication, jsYaml, BrooklynCompletion, CodeMirror,
EditorHtml) {
     var _DEFAULT_BLUEPRINT = 
         'name: Sample Blueprint\n'+
         'description: runs `sleep` for sixty seconds then stops triggering ON_FIRE in Brooklyn\n'+
@@ -171,12 +171,33 @@ define([
                 this.editor = CodeMirror.fromTextArea(this.$("#yaml_code")[0], {
                     lineNumbers: true,
                     viewportMargin: Infinity, /* recommended if height auto */
+                    
                     extraKeys: {"Ctrl-Space": "autocomplete"},
                     mode: {
                         name: "yaml",
                         globalVars: true
+                    },
+                    
+                    hintOptions: {
+                        completeSingle: false,
+//                        closeOnUnfocus: false,   // handy for debugging
                     }
                 });
+                var oldYamlHint = CodeMirror.hint.yaml;
+                var that = this;
+                CodeMirror.hint.yaml = function(cm) {
+                    var result = {from: cm.getCursor(), to: cm.getCursor(), list: [] };
+                    result.list = BrooklynCompletion.getCompletionProposals(that.mode, cm);
+                    
+                    //result.list = result.list.concat(otherwords.list);
+                    // could return other proposals *additionally* but better only if we
found nothing else 
+                    if (!result.list) {
+                        result = CodeMirror.hint.anyword(cm);
+                    }
+                    
+                    return result;
+                };
+                
                 var that = this;
                 this.editor.on("changes", function(editor, changes) {
                     that.refreshOnMinorChange();

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/1058fa7a/src/test/javascript/config.txt
----------------------------------------------------------------------
diff --git a/src/test/javascript/config.txt b/src/test/javascript/config.txt
index 3ea2e75..5052f5e 100644
--- a/src/test/javascript/config.txt
+++ b/src/test/javascript/config.txt
@@ -42,6 +42,8 @@
         "uri":"js/libs/URI",
         "zeroclipboard":"js/libs/ZeroClipboard",
         "js-yaml":"js/libs/js-yaml",
+        "js-yaml-parser":"js/util/code-complete/js-yaml-parser",
+        "brooklyn-yaml-completion-proposals":"js/util/code-complete/brooklyn-yaml-completion-proposals",
 
         "codemirror":"js/libs/codemirror",
         "codemirror-mode-yaml":"js/libs/codemirror/mode/yaml/yaml",


Mime
View raw message