Index: server/static/rpcexplorer/rpc-completer.html |
diff --git a/server/static/rpcexplorer/rpc-completer.html b/server/static/rpcexplorer/rpc-completer.html |
new file mode 100644 |
index 0000000000000000000000000000000000000000..db41603deb5fdcfa45d3209443aa615c84ac3a38 |
--- /dev/null |
+++ b/server/static/rpcexplorer/rpc-completer.html |
@@ -0,0 +1,305 @@ |
+<!-- |
+ Copyright 2016 The Chromium Authors. All rights reserved. |
+ Use of this source code is governed by a BSD-style license 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) { |
+ // 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.findByName(type.desc.field, path[i]); |
+ if (!field) { |
+ console.log('Field ' + path[i] + ' not found') |
+ return []; |
+ } |
+ var typeName = field.type_name; |
+ if (typeof typeName != 'string') { |
+ console.log('Field ' + path[i] + ' is not a message or enum') |
+ return []; |
+ } |
+ // Referenced types are often prefixed with '.'. |
+ typeName = util.trimPrefixDot(typeName); |
+ type = util.resolve(this.description, typeName); |
+ if (!type) { |
+ return []; |
+ } |
+ } |
+ |
+ // Automatically surround with quotes. |
+ var quoteCount = (text.match(/"/g) || []).length; |
+ var shouldQuote = quoteCount % 2 === 0; |
+ |
+ function docTooltip(desc) { |
+ var info = desc.source_code_info; |
+ return info && info.leading_comments || ''; |
+ } |
+ |
+ 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 meta = this.fieldTypeName(field); |
+ if (field.label === 'LABEL_REPEATED') { |
+ meta = 'repeated ' + meta; |
+ } |
+ completions.push({ |
+ caption: field.name, |
+ snippet: this.snippetForField(field, shouldQuote), |
+ 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) { |
+ // snippet docs: |
+ // https://cloud9-sdk.readme.io/docs/snippets |
+ var snippet = field.name; |
+ if (shouldQuote) { |
+ snippet = '"' + snippet + '"'; |
+ } |
+ if (!shouldQuote) { |
+ return snippet; |
+ } |
+ |
+ snippet += ': '; |
+ |
+ var open = ''; |
+ var close = ''; |
+ if (field.label === 'LABEL_REPEATED') { |
+ open += '['; |
+ close = ']' + close; |
+ } |
+ |
+ switch (field.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.type_name); |
+ } |
+ return name; |
+ }, |
+ |
+ _skipWhitespace: function(text, i) { |
+ var space = { |
+ ' ': 1, |
+ '\n': 1, |
+ '\r': 1, |
+ '\t': 1 |
+ }; |
+ while (space[text.charAt(i)]) { |
+ i++; |
+ } |
+ return i; |
+ } |
+ }); |
+</script> |