Chromium Code Reviews| Index: Source/devtools/front_end/languages/LanguageService.js |
| diff --git a/Source/devtools/front_end/languages/LanguageService.js b/Source/devtools/front_end/languages/LanguageService.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a4614a24227292f84c1531c5163b97c502fa6ba0 |
| --- /dev/null |
| +++ b/Source/devtools/front_end/languages/LanguageService.js |
| @@ -0,0 +1,373 @@ |
| +// Copyright 2015 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. |
| + |
| +/** |
| + * @interface |
| + */ |
| +WebInspector.LanguageService = function(){}; |
| +WebInspector.LanguageService.prototype = { |
| + /** |
| + * @return {string} |
| + */ |
| + get name() {}, |
|
pfeldman
2015/08/13 21:15:46
We don't use getters for the new code.
wes
2015/08/14 00:55:05
Oh, okay. I had seen them around elsewhere and was
pfeldman
2015/08/17 21:15:51
I know, we are slowly getting rid of them wherever
|
| + |
| + /** |
| + * @param {string} capability |
| + * @return {boolean} |
| + */ |
| + handlesCapability: function(capability) { return false; } |
|
pfeldman
2015/08/13 21:15:46
capabilities: function getter instead?
wes
2015/08/14 00:55:05
Possible. I can't really think of a use-case where
|
| +} |
| + |
| +/** |
| + * The language service capabilities list determines what actions are added |
| + * to the LanguageService and LanguageService.Handles objects, in addition |
| + * to what methods are searched for on providers to fulfill those capabilities |
| + */ |
| +WebInspector.LanguageService.Capabilities = { |
| + Transpile: "transpile", |
|
pfeldman
2015/08/13 21:15:46
4 space indent everywhere.
wes
2015/08/14 00:55:05
Ach. At least I was internally consistent to the f
pfeldman
2015/08/17 21:15:51
Nope, we don't have a linter :( We use the Blink c
|
| + PopulateContextMenu: "populateContextMenu", |
| + Completions: "completions", |
| + DebuggerCompletions: "debuggerCompletions", |
| +}; |
| + |
| +/** |
| + * @constructor |
| + * @extends {WebInspector.Object} |
| + */ |
| +WebInspector.LanguageServiceManager = function() { |
| + this._override = {}; |
| + this._servicesByMime = {}; |
| + |
| + for (var key in WebInspector.LanguageService.Capabilities) { |
| + var actionName = WebInspector.LanguageService.Capabilities[key]; |
| + this._servicesByMime[Symbol.for(key)] = {}; |
| + WebInspector.LanguageService.prototype[actionName] = function() {}; |
| + } |
| + |
| + var _this = this; |
| + function makeHandlesChecker(key) { |
| + var actionName = WebInspector.LanguageService.Capabilities[key]; |
| + return function(mime) { |
| + var override = _this._override[actionName]; |
| + if (typeof override !== "undefined") { |
| + return override; |
| + } |
| + var service = _this._servicesByMime[Symbol.for(key)][mime]; |
| + return service; |
| + }; |
| + } |
| + |
| + function makeCapabilityAction(key) { |
| + var actionName = WebInspector.LanguageService.Capabilities[key]; |
| + return function(mime) { |
| + var override = _this._override[actionName]; |
| + if (typeof override !== "undefined") { |
| + if (typeof override === "boolean") { |
| + if (override) { |
| + return Promise.resolve(); |
| + } else { |
| + return Promise.reject("No language service present to handle \""+(key)+"\" for mime \""+(mime)+"\"."); |
| + } |
| + } |
| + return override[actionName].apply(override, Array.prototype.slice.call(arguments, 1)); |
| + } |
| + if (!_this.handles[actionName](mime)) { |
| + return Promise.reject("No language service present to handle \""+(key)+"\" for mime \""+(mime)+"\"."); |
| + } |
| + var service = _this._servicesByMime[Symbol.for(key)][mime]; |
| + return service[actionName].apply(service, Array.prototype.slice.call(arguments, 1)); |
| + }; |
| + } |
| + |
| + for (var key in WebInspector.LanguageService.Capabilities) { |
| + _this.handles[WebInspector.LanguageService.Capabilities[key]] = makeHandlesChecker(key); |
| + _this[WebInspector.LanguageService.Capabilities[key]] = makeCapabilityAction(key); |
| + } |
| +}; |
| + |
| +WebInspector.LanguageServiceManager.Events = { |
| + ServiceAdded: "onServiceAdded", |
| + InferredMimeUpdated: "onInferredMimeUpdated" |
| +}; |
| + |
| +/** |
| + * Stub external API here to satisfy closure compiler - actual implementations |
| + * are done in the ctor above. Not sure if there's a better way to type this, while |
| + * still making it simple to 'declare' a new optional member to a service |
| + */ |
| +WebInspector.LanguageServiceManager.prototype = { |
| + |
| + handles: { |
| + /** |
| + * @param {string} mime |
| + * @return {?WebInspector.LanguageService} |
| + */ |
| + transpile: function(mime) {return null;}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @return {?WebInspector.LanguageService} |
| + */ |
| + populateContextMenu: function(mime) {return null;}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @return {?WebInspector.LanguageService} |
| + */ |
| + completions: function(mime) {return null;}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @return {?WebInspector.LanguageService} |
| + */ |
| + debuggerCompletions: function(mime) {return null;} |
| + }, |
| + |
| + /** |
| + * @param {!Array<string>} mimes |
| + * @param {!WebInspector.LanguageService} service |
| + */ |
| + register: function(mimes, service) { |
| + for (var index = 0; index < mimes.length; index++) { |
| + var mime = mimes[index]; |
| + for (var key in WebInspector.LanguageService.Capabilities) { |
| + var capability = WebInspector.LanguageService.Capabilities[key]; |
| + if (service.handlesCapability(capability)) { |
| + this._servicesByMime[Symbol.for(key)][mime] = service; |
| + } |
| + } |
| + |
| + this._servicesByMime[mime] = service; |
| + } |
| + this._onServiceAdded(service, mimes); |
| + }, |
| + |
| + _onServiceAdded(service, mimes) { |
| + this.dispatchEventToListeners(WebInspector.LanguageServiceManager.Events.ServiceAdded, {service, mimes}); |
| + }, |
| + |
| + /** |
| + * @param {string} capability |
| + * @param {!WebInspector.LanguageService=} override |
| + */ |
| + disableContextualHandling: function(capability, override) { |
| + this._override[capability] = override || false; |
| + }, |
| + |
| + /** |
| + * @param {string} capability |
| + */ |
| + enableContextualHandling: function(capability) { |
| + delete this._override[capability]; |
| + }, |
| + |
| + /** |
| + * @param {string=} capability |
| + * @return {!Array<!WebInspector.LanguageService>} |
| + */ |
| + registered: function(capability) { |
| + return [...new Set(Object.keys(capability ? this._servicesByMime[Symbol.for(capability)]||{} : this._servicesByMime).map(k => capability ? this._servicesByMime[Symbol.for(capability)][k] : this._servicesByMime[k]))]; |
| + }, |
| + |
| + /** |
| + * @param {string} mime |
| + * @return {?WebInspector.LanguageService} |
| + */ |
| + for: function(mime) { |
| + return this._servicesByMime[mime]; |
| + }, |
| + |
| + /** |
| + * @param {string} mime |
| + * @param {string} input |
| + * @param {?{source: string, line: number, column: number}=} location |
| + * @return {!Promise<string>} |
| + */ |
| + transpile: function(mime, input, location) {return Promise.reject();}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @param {{source: string, line: number, column: number}} location |
| + * @return {!Promise<!Array<{text: string, callback: !Function}>>} |
| + */ |
| + populateContextMenu: function(mime, location) {return Promise.reject();}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @param {{source: string, line: number, column: number}} location |
| + * @param {string} prefix |
| + * @return {!Promise<!Array<{text: string, details: !Promise<{detail: string, description: string}>}>>} |
| + */ |
| + completions: function(mime, location, prefix) {return Promise.reject();}, |
| + |
| + /** |
| + * @param {string} mime |
| + * @param {string} content |
| + * @param {number} cursorOffset |
| + * @param {string} prefix |
| + * @param {{source: string, line: number, column: number}=} context |
| + * @return {!Promise<!Array<{text: string, details: !Promise<{detail: string, description: string}>}>>} |
| + */ |
| + debuggerCompletions: function(mime, content, cursorOffset, prefix, context) {return Promise.reject();}, |
| + |
| + updateInferredMime: function() { |
| + this.dispatchEventToListeners(WebInspector.LanguageServiceManager.Events.InferredMimeUpdated); |
| + }, |
| + |
| + __proto__: WebInspector.Object.prototype |
| +}; |
| + |
| +/** |
| + * @constructor |
| + * @implements {WebInspector.LanguageService} |
| + */ |
| +WebInspector.LanguageService.CallbackDelegate = function(name, capabilities) { |
| + this._name = name; |
| + |
| + for (var key in WebInspector.LanguageService.Capabilities) { |
| + var actionName = WebInspector.LanguageService.Capabilities[key]; |
| + if (!!capabilities[actionName]) { |
| + this.appendActionHandler(actionName, capabilities[actionName]); |
| + } |
| + } |
| +}; |
| + |
| +WebInspector.LanguageService.CallbackDelegate.prototype = { |
| + /** |
| + * @override |
| + * @return {string} |
| + */ |
| + get name() { |
| + return this._name; |
| + }, |
| + |
| + /** |
| + * Given an action and a function expecting a callback for |
| + * its final argument, modifies it to return a promise and |
| + * appends it to the language service |
| + * |
| + * @param {string} capability |
| + * @param {!Function} func |
| + */ |
| + appendActionHandler: function(capability, func) { |
| + var _this = this; |
| + this[capability] = function() { |
| + var args = arguments; |
| + return new Promise(function(resolve, reject) { |
| + func.call(_this, ...args, resolve); |
| + }); |
| + }; |
| + }, |
| + |
| + /** |
| + * @override |
| + * @param {string} capability |
| + * @return {boolean} |
| + */ |
| + handlesCapability: function(capability) { |
| + return !!this[capability]; |
| + } |
| +}; |
| + |
| +/** |
| + * @constructor |
| + * @param {!Element} selectElement |
| + * @param {!Array<string>} capabilities |
| + */ |
| +WebInspector.LanguageServiceModel = function(selectElement, capabilities) { |
| + this._capabilities = capabilities; |
| + this._selectElement = selectElement; |
| + var newOption = createElement("option"); |
| + newOption.__type = WebInspector.LanguageServiceModel.OptionType.Inferred; |
| + newOption.text = WebInspector.UIString("%s (inferred)", WebInspector.UIString("javascript")); |
| + this._selectElement.appendChild(newOption); |
| + |
| + var jsOption = createElement("option"); |
| + jsOption.__type = WebInspector.LanguageServiceModel.OptionType.Builtin; |
| + jsOption.text = WebInspector.UIString("javascript"); |
| + this._selectElement.appendChild(jsOption); |
| + |
| + WebInspector.languageService.registered().forEach(service => { |
| + this.addOption(service); |
| + }); |
| + |
| + WebInspector.languageService.addEventListener(WebInspector.LanguageServiceManager.Events.ServiceAdded, this._onServiceAdded, this); |
| + WebInspector.languageService.addEventListener(WebInspector.LanguageServiceManager.Events.InferredMimeUpdated, this._onViewSelected, this); |
| + this._selectElement.addEventListener("change", this._languageServiceChanged.bind(this), false); |
| +}; |
| + |
| +WebInspector.LanguageServiceModel.prototype = { |
| + /** |
| + * @param {!WebInspector.LanguageService} service |
| + */ |
| + addOption: function(service) { |
| + var newOption = createElement("option"); |
| + newOption.__service = service; |
| + newOption.__type = WebInspector.LanguageServiceModel.OptionType.External; |
| + newOption.text = service.name |
| + this._selectElement.appendChild(newOption); |
| + }, |
| + |
| + _onServiceAdded: function(evt) { |
| + var service = /** @type {!WebInspector.LanguageService} */(evt.data.service); |
| + this.addOption(service); |
| + }, |
| + |
| + _onViewSelected: function(evt) { |
| + var mime = WebInspector.ResourceType.fromActivePanel(); |
| + var service = WebInspector.languageService.for(mime); |
| + var name = WebInspector.UIString("javascript"); |
| + if (service) { |
| + name = service.name; |
| + } |
| + var opts = this._selectElement.options; |
| + for (var i = 0; i < opts.length; i++) { |
| + var option = opts[i]; |
| + if (option.__type === WebInspector.LanguageServiceModel.OptionType.Inferred) { |
| + option.text = WebInspector.UIString("%s (inferred)", name); |
| + } |
| + } |
| + }, |
| + |
| + _languageServiceChanged: function() { |
| + var option = this._selectedOption(); |
| + switch(option.__type) { |
| + case WebInspector.LanguageServiceModel.OptionType.Inferred: |
| + this._capabilities.forEach(c => { |
| + WebInspector.languageService.enableContextualHandling(c); |
| + }); |
| + break; |
| + case WebInspector.LanguageServiceModel.OptionType.Builtin: |
| + this._capabilities.forEach(c => { |
| + WebInspector.languageService.disableContextualHandling(c); |
| + }); |
| + break; |
| + case WebInspector.LanguageServiceModel.OptionType.External: |
| + this._capabilities.forEach(c => { |
| + WebInspector.languageService.disableContextualHandling(c, option.__service); |
| + }); |
| + break; |
| + default: |
| + break; |
| + } |
| + }, |
| + |
| + /** |
| + * @return {?Element} |
| + */ |
| + _selectedOption: function() |
| + { |
| + if (this._selectElement.selectedIndex >= 0) |
| + return this._selectElement[this._selectElement.selectedIndex]; |
| + return null; |
| + } |
| +}; |
| + |
| +WebInspector.LanguageServiceModel.OptionType = { |
| + Inferred: "Inferred", |
| + Builtin: "Builtin", |
| + External: "External" |
| +}; |
| + |
| +WebInspector.languageService = new WebInspector.LanguageServiceManager(); |