src/languageTools/LanguageClientWrapper.js
/*
* Copyright (c) 2019 - present Adobe. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*eslint no-console: 0*/
/*eslint indent: 0*/
/*eslint max-len: ["error", { "code": 200 }]*/
define(function (require, exports, module) {
"use strict";
var ToolingInfo = JSON.parse(require("text!languageTools/ToolingInfo.json")),
MESSAGE_FORMAT = {
BRACKETS: "brackets",
LSP: "lsp"
};
function _addTypeInformation(type, params) {
return {
type: type,
params: params
};
}
function hasValidProp(obj, prop) {
return (obj && obj[prop] !== undefined && obj[prop] !== null);
}
function hasValidProps(obj, props) {
var retval = !!obj,
len = props.length,
i;
for (i = 0; retval && (i < len); i++) {
retval = (retval && obj[props[i]] !== undefined && obj[props[i]] !== null);
}
return retval;
}
/*
RequestParams creator - sendNotifications/request
*/
function validateRequestParams(type, params) {
var validatedParams = null;
params = params || {};
//Don't validate if the formatting is done by the caller
if (params.format === MESSAGE_FORMAT.LSP) {
return params;
}
switch (type) {
case ToolingInfo.LANGUAGE_SERVICE.START:
{
if (hasValidProp(params, "rootPaths") || hasValidProp(params, "rootPath")) {
validatedParams = params;
validatedParams.capabilities = validatedParams.capabilities || false;
}
break;
}
case ToolingInfo.FEATURES.CODE_HINTS:
case ToolingInfo.FEATURES.PARAMETER_HINTS:
case ToolingInfo.FEATURES.JUMP_TO_DECLARATION:
case ToolingInfo.FEATURES.JUMP_TO_DEFINITION:
case ToolingInfo.FEATURES.JUMP_TO_IMPL:
{
if (hasValidProps(params, ["filePath", "cursorPos"])) {
validatedParams = params;
}
break;
}
case ToolingInfo.FEATURES.CODE_HINT_INFO:
{
validatedParams = params;
break;
}
case ToolingInfo.FEATURES.FIND_REFERENCES:
{
if (hasValidProps(params, ["filePath", "cursorPos"])) {
validatedParams = params;
validatedParams.includeDeclaration = validatedParams.includeDeclaration || false;
}
break;
}
case ToolingInfo.FEATURES.DOCUMENT_SYMBOLS:
{
if (hasValidProp(params, "filePath")) {
validatedParams = params;
}
break;
}
case ToolingInfo.FEATURES.PROJECT_SYMBOLS:
{
if (hasValidProp(params, "query") && typeof params.query === "string") {
validatedParams = params;
}
break;
}
case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST:
{
validatedParams = params;
}
}
return validatedParams;
}
/*
ReponseParams transformer - used by OnNotifications
*/
function validateNotificationParams(type, params) {
var validatedParams = null;
params = params || {};
//Don't validate if the formatting is done by the caller
if (params.format === MESSAGE_FORMAT.LSP) {
return params;
}
switch (type) {
case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED:
{
if (hasValidProps(params, ["filePath", "fileContent", "languageId"])) {
validatedParams = params;
}
break;
}
case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED:
{
if (hasValidProps(params, ["filePath", "fileContent"])) {
validatedParams = params;
}
break;
}
case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED:
{
if (hasValidProp(params, "filePath")) {
validatedParams = params;
}
break;
}
case ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED:
{
if (hasValidProp(params, "filePath")) {
validatedParams = params;
}
break;
}
case ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED:
{
if (hasValidProps(params, ["foldersAdded", "foldersRemoved"])) {
validatedParams = params;
}
break;
}
case ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION:
{
validatedParams = params;
}
}
return validatedParams;
}
function validateHandler(handler) {
var retval = false;
if (handler && typeof handler === "function") {
retval = true;
} else {
console.warn("Handler validation failed. Handler should be of type 'function'. Provided handler is of type :", typeof handler);
}
return retval;
}
function LanguageClientWrapper(name, path, domainInterface, languages) {
this._name = name;
this._path = path;
this._domainInterface = domainInterface;
this._languages = languages || [];
this._startClient = null;
this._stopClient = null;
this._notifyClient = null;
this._requestClient = null;
this._onRequestHandler = {};
this._onNotificationHandlers = {};
this._dynamicCapabilities = {};
this._serverCapabilities = {};
//Initialize with keys for brackets events we want to tap into.
this._onEventHandlers = {
"activeEditorChange": [],
"projectOpen": [],
"beforeProjectClose": [],
"dirtyFlagChange": [],
"documentChange": [],
"fileNameChange": [],
"beforeAppClose": []
};
this._init();
}
LanguageClientWrapper.prototype._init = function () {
this._domainInterface.registerMethods([
{
methodName: ToolingInfo.LANGUAGE_SERVICE.REQUEST,
methodHandle: this._onRequestDelegator.bind(this)
},
{
methodName: ToolingInfo.LANGUAGE_SERVICE.NOTIFY,
methodHandle: this._onNotificationDelegator.bind(this)
}
]);
//create function interfaces
this._startClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.START, true);
this._stopClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.STOP, true);
this._notifyClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.NOTIFY);
this._requestClient = this._domainInterface.createInterface(ToolingInfo.LANGUAGE_SERVICE.REQUEST, true);
};
LanguageClientWrapper.prototype._onRequestDelegator = function (params) {
if (!params || !params.type) {
console.log("Invalid server request");
return $.Deferred().reject();
}
var requestHandler = this._onRequestHandler[params.type];
if (params.type === ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST) {
return this._registrationShim(params.params, requestHandler);
}
if (params.type === ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST) {
return this._unregistrationShim(params.params, requestHandler);
}
if (validateHandler(requestHandler)) {
return requestHandler.call(null, params.params);
}
console.log("No handler provided for server request type : ", params.type);
return $.Deferred().reject();
};
LanguageClientWrapper.prototype._onNotificationDelegator = function (params) {
if (!params || !params.type) {
console.log("Invalid server notification");
return;
}
var notificationHandlers = this._onNotificationHandlers[params.type];
if (notificationHandlers && Array.isArray(notificationHandlers) && notificationHandlers.length) {
notificationHandlers.forEach(function (handler) {
if (validateHandler(handler)) {
handler.call(null, params.params);
}
});
} else {
console.log("No handlers provided for server notification type : ", params.type);
}
};
LanguageClientWrapper.prototype._request = function (type, params) {
params = validateRequestParams(type, params);
if (params) {
params = _addTypeInformation(type, params);
return this._requestClient(params);
}
console.log("Invalid Parameters provided for request type : ", type);
return $.Deferred().reject();
};
LanguageClientWrapper.prototype._notify = function (type, params) {
params = validateNotificationParams(type, params);
if (params) {
params = _addTypeInformation(type, params);
this._notifyClient(params);
} else {
console.log("Invalid Parameters provided for notification type : ", type);
}
};
LanguageClientWrapper.prototype._addOnRequestHandler = function (type, handler) {
if (validateHandler(handler)) {
this._onRequestHandler[type] = handler;
}
};
LanguageClientWrapper.prototype._addOnNotificationHandler = function (type, handler) {
if (validateHandler(handler)) {
if (!this._onNotificationHandlers[type]) {
this._onNotificationHandlers[type] = [];
}
this._onNotificationHandlers[type].push(handler);
}
};
/**
Requests
*/
//start
LanguageClientWrapper.prototype.start = function (params) {
params = validateRequestParams(ToolingInfo.LANGUAGE_SERVICE.START, params);
if (params) {
var self = this;
return this._startClient(params)
.then(function (result) {
self.setServerCapabilities(result.capabilities);
return $.Deferred().resolve(result);
}, function (err) {
return $.Deferred().reject(err);
});
}
console.log("Invalid Parameters provided for request type : start");
return $.Deferred().reject();
};
//shutdown
LanguageClientWrapper.prototype.stop = function () {
return this._stopClient();
};
//restart
LanguageClientWrapper.prototype.restart = function (params) {
var self = this;
return this.stop().then(function () {
return self.start(params);
});
};
/**
textDocument requests
*/
//completion
LanguageClientWrapper.prototype.requestHints = function (params) {
return this._request(ToolingInfo.FEATURES.CODE_HINTS, params)
.then(function(response) {
if(response && response.items && response.items.length) {
logAnalyticsData("CODE_HINTS");
}
return $.Deferred().resolve(response);
}, function(err) {
return $.Deferred().reject(err);
});
};
//completionItemResolve
LanguageClientWrapper.prototype.getAdditionalInfoForHint = function (params) {
return this._request(ToolingInfo.FEATURES.CODE_HINT_INFO, params);
};
//signatureHelp
LanguageClientWrapper.prototype.requestParameterHints = function (params) {
return this._request(ToolingInfo.FEATURES.PARAMETER_HINTS, params)
.then(function(response) {
if (response && response.signatures && response.signatures.length) {
logAnalyticsData("PARAM_HINTS");
}
return $.Deferred().resolve(response);
}, function(err) {
return $.Deferred().reject(err);
});
};
//gotoDefinition
LanguageClientWrapper.prototype.gotoDefinition = function (params) {
return this._request(ToolingInfo.FEATURES.JUMP_TO_DEFINITION, params)
.then(function(response) {
if(response && response.range) {
logAnalyticsData("JUMP_TO_DEF");
}
return $.Deferred().resolve(response);
}, function(err) {
return $.Deferred().reject(err);
});
};
//gotoDeclaration
LanguageClientWrapper.prototype.gotoDeclaration = function (params) {
return this._request(ToolingInfo.FEATURES.JUMP_TO_DECLARATION, params);
};
//gotoImplementation
LanguageClientWrapper.prototype.gotoImplementation = function (params) {
return this._request(ToolingInfo.FEATURES.JUMP_TO_IMPL, params);
};
//findReferences
LanguageClientWrapper.prototype.findReferences = function (params) {
return this._request(ToolingInfo.FEATURES.FIND_REFERENCES, params);
};
//documentSymbol
LanguageClientWrapper.prototype.requestSymbolsForDocument = function (params) {
return this._request(ToolingInfo.FEATURES.DOCUMENT_SYMBOLS, params);
};
/**
workspace requests
*/
//workspaceSymbol
LanguageClientWrapper.prototype.requestSymbolsForWorkspace = function (params) {
return this._request(ToolingInfo.FEATURES.PROJECT_SYMBOLS, params);
};
//These will mostly be callbacks/[done-fail](promises)
/**
Window OnNotifications
*/
//showMessage
LanguageClientWrapper.prototype.addOnShowMessage = function (handler) {
this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.SHOW_MESSAGE, handler);
};
//logMessage
LanguageClientWrapper.prototype.addOnLogMessage = function (handler) {
this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.LOG_MESSAGE, handler);
};
/**
healthData/logging OnNotifications
*/
//telemetry
LanguageClientWrapper.prototype.addOnTelemetryEvent = function (handler) {
this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.TELEMETRY, handler);
};
/**
textDocument OnNotifications
*/
//onPublishDiagnostics
LanguageClientWrapper.prototype.addOnCodeInspection = function (handler) {
this._addOnNotificationHandler(ToolingInfo.SERVICE_NOTIFICATIONS.DIAGNOSTICS, handler);
};
/**
Window OnRequest
*/
//showMessageRequest - handler must return promise
LanguageClientWrapper.prototype.onShowMessageWithRequest = function (handler) {
this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.SHOW_SELECT_MESSAGE, handler);
};
LanguageClientWrapper.prototype.onProjectFoldersRequest = function (handler) {
this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.PROJECT_FOLDERS_REQUEST, handler);
};
LanguageClientWrapper.prototype._registrationShim = function (params, handler) {
var self = this;
var registrations = params.registrations;
registrations.forEach(function (registration) {
var id = registration.id;
self._dynamicCapabilities[id] = registration;
});
return validateHandler(handler) ? handler(params) : $.Deferred().resolve();
};
LanguageClientWrapper.prototype.onDynamicCapabilityRegistration = function (handler) {
this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.REGISTRATION_REQUEST, handler);
};
LanguageClientWrapper.prototype._unregistrationShim = function (params, handler) {
var self = this;
var unregistrations = params.unregistrations;
unregistrations.forEach(function (unregistration) {
var id = unregistration.id;
delete self._dynamicCapabilities[id];
});
return validateHandler(handler) ? handler(params) : $.Deferred().resolve();
};
LanguageClientWrapper.prototype.onDynamicCapabilityUnregistration = function (handler) {
this._addOnRequestHandler(ToolingInfo.SERVICE_REQUESTS.UNREGISTRATION_REQUEST, handler);
};
/*
Unimplemented OnNotifications
workspace
applyEdit (codeAction, codeLens)
*/
/**
SendNotifications
*/
/**
workspace SendNotifications
*/
//didChangeProjectRoots
LanguageClientWrapper.prototype.notifyProjectRootsChanged = function (params) {
this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.PROJECT_FOLDERS_CHANGED, params);
};
/**
textDocument SendNotifications
*/
//didOpenTextDocument
LanguageClientWrapper.prototype.notifyTextDocumentOpened = function (params) {
this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_OPENED, params);
};
//didCloseTextDocument
LanguageClientWrapper.prototype.notifyTextDocumentClosed = function (params) {
this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CLOSED, params);
};
//didChangeTextDocument
LanguageClientWrapper.prototype.notifyTextDocumentChanged = function (params) {
this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_CHANGED, params);
};
//didSaveTextDocument
LanguageClientWrapper.prototype.notifyTextDocumentSave = function (params) {
this._notify(ToolingInfo.SYNCHRONIZE_EVENTS.DOCUMENT_SAVED, params);
};
/**
Custom messages
*/
//customNotification
LanguageClientWrapper.prototype.sendCustomNotification = function (params) {
this._notify(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_NOTIFICATION, params);
};
LanguageClientWrapper.prototype.onCustomNotification = function (type, handler) {
this._addOnNotificationHandler(type, handler);
};
//customRequest
LanguageClientWrapper.prototype.sendCustomRequest = function (params) {
return this._request(ToolingInfo.LANGUAGE_SERVICE.CUSTOM_REQUEST, params);
};
LanguageClientWrapper.prototype.onCustomRequest = function (type, handler) {
this._addOnRequestHandler(type, handler);
};
//Handling Brackets Events
LanguageClientWrapper.prototype.addOnEditorChangeHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["activeEditorChange"].push(handler);
}
};
LanguageClientWrapper.prototype.addOnProjectOpenHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["projectOpen"].push(handler);
}
};
LanguageClientWrapper.prototype.addBeforeProjectCloseHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["beforeProjectClose"].push(handler);
}
};
LanguageClientWrapper.prototype.addOnDocumentDirtyFlagChangeHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["dirtyFlagChange"].push(handler);
}
};
LanguageClientWrapper.prototype.addOnDocumentChangeHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["documentChange"].push(handler);
}
};
LanguageClientWrapper.prototype.addOnFileRenameHandler = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["fileNameChange"].push(handler);
}
};
LanguageClientWrapper.prototype.addBeforeAppClose = function (handler) {
if (validateHandler(handler)) {
this._onEventHandlers["beforeAppClose"].push(handler);
}
};
LanguageClientWrapper.prototype.addOnCustomEventHandler = function (eventName, handler) {
if (validateHandler(handler)) {
if (!this._onEventHandlers[eventName]) {
this._onEventHandlers[eventName] = [];
}
this._onEventHandlers[eventName].push(handler);
}
};
LanguageClientWrapper.prototype.triggerEvent = function (event) {
var eventName = event.type,
eventArgs = arguments;
if (this._onEventHandlers[eventName] && Array.isArray(this._onEventHandlers[eventName])) {
var handlers = this._onEventHandlers[eventName];
handlers.forEach(function (handler) {
if (validateHandler(handler)) {
handler.apply(null, eventArgs);
}
});
}
};
LanguageClientWrapper.prototype.getDynamicCapabilities = function () {
return this._dynamicCapabilities;
};
LanguageClientWrapper.prototype.getServerCapabilities = function () {
return this._serverCapabilities;
};
LanguageClientWrapper.prototype.setServerCapabilities = function (serverCapabilities) {
this._serverCapabilities = serverCapabilities;
};
exports.LanguageClientWrapper = LanguageClientWrapper;
function logAnalyticsData(typeStrKey) {
var editor = require("editor/EditorManager").getActiveEditor(),
document = editor ? editor.document : null,
language = document ? document.language : null,
languageName = language ? language._name : "",
HealthLogger = require("utils/HealthLogger"),
typeStr = HealthLogger.commonStrings[typeStrKey] || "";
HealthLogger.sendAnalyticsData(
HealthLogger.commonStrings.USAGE + HealthLogger.commonStrings.LANGUAGE_SERVER_PROTOCOL + typeStr + languageName,
HealthLogger.commonStrings.USAGE,
HealthLogger.commonStrings.LANGUAGE_SERVER_PROTOCOL,
typeStr,
languageName.toLowerCase()
);
}
//For unit testting
exports.validateRequestParams = validateRequestParams;
exports.validateNotificationParams = validateNotificationParams;
});