| <!-- |
| 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; |
| } |
| |
| 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.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> |