blob: a43c232624c8104f9bedf9b974c3c21fe459a1a3 [file] [log] [blame]
<!--
Copyright 2016 The LUCI Authors. All rights reserved.
Use of this source code is governed under the Apache License, Version 2.0
that can be found in the LICENSE file.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="rpc-descriptor-util.html">
<!--
The `rpc-completer` element implements Ace editor completer interface
based on a protobuf message descriptors.
-->
<script>
'use strict';
Polymer({
is: 'rpc-completer',
properties: {
/** @type {FileDescriptorSet} */
description: Object,
rootTypeName: String,
/** @type {DescriptorProto} */
rootType: {
type: Object,
computed: '_resolveType(description, rootTypeName)'
}
},
/**
* Returns elements to display in autocomplete.
*/
getCompletions: function(editor, session, pos, prefix, callback) {
if (!this.rootType) {
return;
}
// Get all text left to the current selection.
var beforePos = {
start: {row: 0, col: 0},
end: session.selection.getRange().start
};
var text = session.getTextRange(beforePos);
var completions = this.getCompletionsForText(this.rootType, text);
if (completions) {
callback(null, completions);
}
},
/**
* Returns leading comments of a completion.
* The result is displayed to the right of the selected completion.
*/
getDocTooltip: function(completion) {
return completion.docTooltip;
},
getCompletionsForText: function(type, text) {
if (!type) {
return [];
}
// Resolve path.
var path = this.getCurrentPath(text);
if (path == null) {
return [];
}
// Resolve type.
var util = rpcExplorer.descUtil;
for (var i = 0; i < path.length; i++) {
if (type.type != 'messageType') {
return [];
}
var field = util.findByJsonName(type.desc.field, path[i]);
if (!field) {
console.log('Field ' + path[i] + ' not found');
return [];
}
type = field.rpcExpTypeInfo;
if (!type) {
return [];
}
if (type.desc.options && type.desc.options.mapEntry) {
// map<K, V> fields are converted to repeated messages, where the
// message has fields "key" and "value".
// JSONPB, however, expects a object with keys and values,
// not an array of key-value objects.
if (i + 1 == path.length) {
// We don't have completions for key values.
return [];
} else {
// path[i+1] is key value which is irrelevant for completions.
// Get completions for the "value" field of the map entry type.
path[i + 1] = 'value';
}
}
}
// Automatically surround with quotes.
var quoteCount = (text.match(/"/g) || []).length;
var shouldQuote = quoteCount % 2 === 0;
function docTooltip(desc) {
var info = desc.sourceCodeInfo;
return info && info.leadingComments || '';
}
var completions = [];
switch (type.type) {
case 'messageType':
if (!type.desc.field) {
break;
}
for (var i = 0; i < type.desc.field.length; i++) {
var field = type.desc.field[i];
var fType = field.rpcExpTypeInfo && field.rpcExpTypeInfo.desc;
var isMap = fType && fType.options && fType.options.mapEntry;
var meta;
if (isMap) {
var keyType = this.fieldTypeName(fType.field[0]);
var valueType = this.fieldTypeName(fType.field[1]);
meta = 'map<' + keyType + ', ' + valueType + '>';
} else {
meta = this.fieldTypeName(field);
if (field.label === 'LABEL_REPEATED') {
meta = 'repeated ' + meta;
}
}
completions.push({
caption: field.jsonName,
snippet: this.snippetForField(field, shouldQuote, isMap),
meta: meta,
docTooltip: docTooltip(field)
});
}
break;
case 'enumType':
for (var i = 0; i < type.desc.value.length; i++) {
var value = type.desc.value[i];
var snippet = value.name;
if (shouldQuote) {
snippet = '"' + snippet + '"';
}
completions.push({
caption: value.name,
snippet: snippet,
meta: '' + value.number,
docTooltip: docTooltip(value)
});
}
break;
}
return completions;
},
snippetForField: function(field, shouldQuote, isMap) {
// snippet docs:
// https://cloud9-sdk.readme.io/docs/snippets
var snippet = field.jsonName;
if (shouldQuote) {
snippet = '"' + snippet + '"';
}
if (!shouldQuote) {
return snippet;
}
snippet += ': ';
var open = '';
var close = '';
if (isMap) {
open = '{';
close = '}';
} else {
if (field.label === 'LABEL_REPEATED') {
open += '[';
close = ']' + close;
}
let type = field.type;
if (field.typeName == '.google.protobuf.FieldMask') {
// google.protobuf.FieldMask is a message, but in JSON it is encoded
// as a string.
// https://github.com/google/protobuf/blob/f8262db9191280d9e8ed0b2f4a77f71f86960c46/src/google/protobuf/field_mask.proto#L187
type = 'TYPE_STRING';
}
switch (type) {
case 'TYPE_MESSAGE':
open += '{';
close = '}' + close;
break;
case 'TYPE_STRING':
case 'TYPE_ENUM':
open += '"';
close = '"' + close;
break;
}
}
// ${0} is the position of cursor after insertion.
snippet += open + '${0}' + close;
return snippet;
},
/**
* Resolves path at the end of text, best effort.
* e.g. for text '{ "a": { "b": [' returns ['a', 'b']
* For '{ "a": {}, "b": {' returns ['b'].
* For '{ "a":' returns ['a'].
*/
getCurrentPath: function(text) {
var path = [];
for (var i = 0; i < text.length;) {
i = text.indexOf(':', i);
if (i === -1) {
break;
}
var colon = i;
i++;
i = this._skipWhitespace(text, i);
if (i === text.length ||
text.charAt(i) === '"' && i+1 === text.length) {
// the path is a field.
} else if (text.charAt(i) in {'{':0, '[': 0}) {
// there is an array or object after the colon
var closingIndex = this.findMatching(text, i);
if (closingIndex !== -1) {
// Not an object/array or closed. Ignore.
continue;
}
} else {
continue
}
// read the name to the left of colon.
var secondQuote = text.lastIndexOf('"', colon);
if (secondQuote === -1) {
return null;
}
var firstQuote = text.lastIndexOf('"', secondQuote - 1);
if (firstQuote === -1) {
return null;
}
path.push(text.substring(firstQuote + 1, secondQuote));
}
return path;
},
/** Finds index of the matching brace. */
findMatching: function(text, i) {
var level = 0;
var open = text.charAt(i);
var close;
switch (open) {
case '{':
close = '}';
break;
case '[':
close = ']';
break;
default:
throw Error('Unexpected brace: ' + open);
}
for (; i < text.length; i++) {
switch (text.charAt(i)) {
case open:
level++;
break;
case close:
level--;
if (level === 0) {
return i;
}
break;
}
}
return -1;
},
_resolveType: function(desc, name) {
return rpcExplorer.descUtil.resolve(desc, name);
},
_scalarTypeNames: {
TYPE_DOUBLE: 'double',
TYPE_FLOAT: 'float',
TYPE_INT64: 'int64',
TYPE_UINT64: 'uint64',
TYPE_INT32: 'int32',
TYPE_FIXED64: 'fixed64',
TYPE_FIXED32: 'fixed32',
TYPE_BOOL: 'bool',
TYPE_STRING: 'string',
TYPE_BYTES: 'bytes',
TYPE_UINT32: 'uint32',
TYPE_SFIXED32: 'sfixed32',
TYPE_SFIXED64: 'sfixed64',
TYPE_SINT32: 'sint32',
TYPE_SINT64: 'sint64',
},
fieldTypeName: function(field) {
var name = this._scalarTypeNames[field.type];
if (!name) {
name = rpcExplorer.descUtil.trimPrefixDot(field.typeName);
}
return name;
},
_skipWhitespace: function(text, i) {
var space = {
' ': 1,
'\n': 1,
'\r': 1,
'\t': 1
};
while (space[text.charAt(i)]) {
i++;
}
return i;
}
});
</script>