blob: 60b2fda63ca7627640a5ac1324b2e39a1af88286 [file] [log] [blame]
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" });
// if browser opened while server is starting this will fail on first completion
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 (n.kind == 'sequence') {
if (typeof n.result.length === 'undefined') {
console.log("WARN: mismatch expected list but had no length", n);
}
n.type = 'list';
} else if (n.kind == 'mapping') {
if (typeof n.result.length !== 'undefined') {
console.log("WARN: mismatch expected map but had length", n);
}
n.type = 'map';
} else {
if (n.kind != 'scalar') {
console.log("WARN: mismatch expected scalar/primitive", n);
}
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(node) {
var result = [];
if (!node || !node.name)
result = result.concat( { displayText: "name:", text: "name: " } );
if (!node || !node.services)
result = result.concat( { displayText: "services:", text: "services:\n- type: " } );
if (!node || !(node.location || node.locations))
result = result.concat( { displayText: "location:", text: "location:\n " } );
return result;
},
getServiceTypes: function(n) {
var result = [];
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;
var fromHereToEndOfOuterBlock;
if (nn[1].key) {
fromHereToEndOfOuterBlock = position <= nn[1].end ? nn[1].doc.substring(position, nn[1].end) : nn[1].doc.substring(nn[1].end, position);
fromHereToEndOfOuterBlock = fromHereToEndOfOuterBlock.trim();
}
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 && fromHereToEndOfOuterBlock.length==0) {
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
// finally add root proposals
if (cmPosition.ch==0 && fromHereToEndOfOuterBlock && fromHereToEndOfOuterBlock.length==0) {
result = result.concat(this.getRootProposals(nn[0] ? nn[0].result : null));
}
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') {
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(n.result);
} 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;
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;
});