1
0
mirror of https://github.com/adobe/brackets.git synced 2024-11-20 01:42:55 +01:00

Php Tooling Extensions Using LSP Framework (#14671)

* Php Tooling Extensions Using LSP Framework

* Corrected Indentation space

* Corrected ESLint Error

* addressed review comments

* Addressed review comments

* addressed review comments

* Added Preferences description string

* Addressed review comments

* Addressed review comments

* addressed review comments

* Addresed review comments

* Addresed review comments

* Addressed some bug

* Adding Unit test Files

* Corrected the strings

* Fixed Eslint Errors

* Added some unit test Cases

* using restart function

* Switch to php7 in travis before dependency installation

php-Tooling npm package requires felixfbecker/language-server php
package, which can only be installed on php7. this should fix the break
in brackets travis build
This commit is contained in:
Nitesh Kumar 2019-04-03 14:12:03 +05:30 committed by Shubham Yadav
parent b977dff1d7
commit 9516c8de26
14 changed files with 1163 additions and 2 deletions

View File

@ -2,6 +2,8 @@ language: node_js
sudo: false # use container-based Travis infrastructure
node_js:
- "6"
before_install:
- phpenv global 7.0 #switch to php7, since that's what php-Tooling extension requires
before_script:
- npm install -g grunt-cli
- npm install -g jasmine-node

View File

@ -0,0 +1,160 @@
/*
* 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-disable indent */
/* eslint max-len: ["error", { "code": 200 }]*/
define(function (require, exports, module) {
"use strict";
var _ = brackets.getModule("thirdparty/lodash");
var DefaultProviders = brackets.getModule("languageTools/DefaultProviders"),
EditorManager = brackets.getModule('editor/EditorManager'),
TokenUtils = brackets.getModule("utils/TokenUtils"),
StringMatch = brackets.getModule("utils/StringMatch"),
matcher = new StringMatch.StringMatcher({
preferPrefixMatches: true
});
var phpSuperGlobalVariables = JSON.parse(require("text!phpGlobals.json"));
function CodeHintsProvider(client) {
this.defaultCodeHintProviders = new DefaultProviders.CodeHintsProvider(client);
}
function formatTypeDataForToken($hintObj, token) {
$hintObj.addClass('brackets-hints-with-type-details');
if (token.detail) {
if (token.detail.trim() !== '?') {
if (token.detail.length < 30) {
$('<span>' + token.detail.split('->').join(':').toString().trim() + '</span>').appendTo($hintObj).addClass("brackets-hints-type-details");
}
$('<span>' + token.detail.split('->').join(':').toString().trim() + '</span>').appendTo($hintObj).addClass("hint-description");
}
} else {
if (token.keyword) {
$('<span>keyword</span>').appendTo($hintObj).addClass("brackets-hints-keyword");
}
}
if (token.documentation) {
$hintObj.attr('title', token.documentation);
$('<span></span>').text(token.documentation.trim()).appendTo($hintObj).addClass("hint-doc");
}
}
function filterWithQueryAndMatcher(hints, query) {
var matchResults = $.map(hints, function (hint) {
var searchResult = matcher.match(hint.label, query);
if (searchResult) {
for (var key in hint) {
searchResult[key] = hint[key];
}
}
return searchResult;
});
return matchResults;
}
CodeHintsProvider.prototype.hasHints = function (editor, implicitChar) {
return this.defaultCodeHintProviders.hasHints(editor, implicitChar);
};
CodeHintsProvider.prototype.getHints = function (implicitChar) {
if (!this.defaultCodeHintProviders.client) {
return null;
}
var editor = EditorManager.getActiveEditor(),
pos = editor.getCursorPos(),
docPath = editor.document.file._path,
$deferredHints = $.Deferred(),
self = this.defaultCodeHintProviders;
this.defaultCodeHintProviders.client.requestHints({
filePath: docPath,
cursorPos: pos
}).done(function (msgObj) {
var context = TokenUtils.getInitialContext(editor._codeMirror, pos),
hints = [];
self.query = context.token.string.slice(0, context.pos.ch - context.token.start);
if (msgObj) {
var res = msgObj.items || [];
// There is a bug in Php Language Server, Php Language Server does not provide superGlobals
// Variables as completion. so these variables are being explicity put in response objects
// below code should be removed if php server fix this bug.
if(self.query) {
for(var key in phpSuperGlobalVariables) {
res.push({
label: key,
documentation: phpSuperGlobalVariables[key].description,
detail: phpSuperGlobalVariables[key].type
});
}
}
var filteredHints = filterWithQueryAndMatcher(res, self.query);
StringMatch.basicMatchSort(filteredHints);
filteredHints.forEach(function (element) {
var $fHint = $("<span>")
.addClass("brackets-hints");
if (element.stringRanges) {
element.stringRanges.forEach(function (item) {
if (item.matched) {
$fHint.append($("<span>")
.append(_.escape(item.text))
.addClass("matched-hint"));
} else {
$fHint.append(_.escape(item.text));
}
});
} else {
$fHint.text(element.label);
}
$fHint.data("token", element);
formatTypeDataForToken($fHint, element);
hints.push($fHint);
});
}
$deferredHints.resolve({
"hints": hints
});
}).fail(function () {
$deferredHints.reject();
});
return $deferredHints;
};
CodeHintsProvider.prototype.insertHint = function ($hint) {
return this.defaultCodeHintProviders.insertHint($hint);
};
exports.CodeHintsProvider = CodeHintsProvider;
});

View File

@ -0,0 +1,120 @@
/*
* 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.
*
*/
/*global exports */
/*global process */
/*eslint-env es6, node*/
/*eslint max-len: ["error", { "code": 200 }]*/
"use strict";
var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient,
net = require("net"),
cp = require("child_process"),
execa = require("execa"),
semver = require('semver'),
clientName = "PhpClient",
executablePath = "",
memoryLimit = "";
function validatePhpExecutable(confParams) {
executablePath = confParams["executablePath"] ||
(process.platform === 'win32' ? 'php.exe' : 'php');
memoryLimit = confParams["memoryLimit"] || '4095M';
return new Promise(function (resolve, reject) {
if (memoryLimit !== '-1' && !/^\d+[KMG]?$/.exec(memoryLimit)) {
reject("PHP_SERVER_MEMORY_LIMIT_INVALID");
return;
}
execa.stdout(executablePath, ['--version']).then(function (output) {
var matchStr = output.match(/^PHP ([^\s]+)/m);
if (!matchStr) {
reject("PHP_VERSION_INVALID");
return;
}
var version = matchStr[1].split('-')[0];
if (!/^\d+.\d+.\d+$/.test(version)) {
version = version.replace(/(\d+.\d+.\d+)/, '$1-');
}
if (semver.lt(version, '7.0.0')) {
reject(["PHP_UNSUPPORTED_VERSION", version]);
return;
}
resolve();
}).catch(function (err) {
if (err.code === 'ENOENT') {
reject("PHP_EXECUTABLE_NOT_FOUND");
} else {
reject(["PHP_PROCESS_SPAWN_ERROR", err.code]);
console.error(err);
}
return;
});
});
}
var serverOptions = function () {
return new Promise(function (resolve, reject) {
var server = net.createServer(function (socket) {
console.log('PHP process connected');
socket.on('end', function () {
console.log('PHP process disconnected');
});
server.close();
resolve({
reader: socket,
writer: socket
});
});
server.listen(0, '127.0.0.1', function () {
var pathToPHP = __dirname + "/vendor/felixfbecker/language-server/bin/php-language-server.php";
var childProcess = cp.spawn(executablePath, [
pathToPHP,
'--tcp=127.0.0.1:' + server.address().port,
'--memory-limit=' + memoryLimit
]);
childProcess.stderr.on('data', function (chunk) {
var str = chunk.toString();
console.log('PHP Language Server:', str);
});
childProcess.on('exit', function (code, signal) {
console.log(
"Language server exited " + (signal ? "from signal " + signal : "with exit code " + code)
);
});
return childProcess;
});
});
},
options = {
serverOptions: serverOptions
};
function init(domainManager) {
var client = new LanguageClient(clientName, domainManager, options);
client.addOnRequestHandler('validatePhpExecutable', validatePhpExecutable);
}
exports.init = init;

View File

@ -0,0 +1,7 @@
{
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"felixfbecker/language-server": "^5.4"
}
}

View File

@ -0,0 +1,231 @@
/*
* 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.
*
*/
define(function (require, exports, module) {
"use strict";
var LanguageTools = brackets.getModule("languageTools/LanguageTools"),
AppInit = brackets.getModule("utils/AppInit"),
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
ProjectManager = brackets.getModule("project/ProjectManager"),
EditorManager = brackets.getModule("editor/EditorManager"),
LanguageManager = brackets.getModule("language/LanguageManager"),
CodeHintManager = brackets.getModule("editor/CodeHintManager"),
ParameterHintManager = brackets.getModule("features/ParameterHintsManager"),
JumpToDefManager = brackets.getModule("features/JumpToDefManager"),
CodeInspection = brackets.getModule("language/CodeInspection"),
DefaultProviders = brackets.getModule("languageTools/DefaultProviders"),
CodeHintsProvider = require("CodeHintsProvider").CodeHintsProvider,
DefaultEventHandlers = brackets.getModule("languageTools/DefaultEventHandlers"),
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
Strings = brackets.getModule("strings"),
Dialogs = brackets.getModule("widgets/Dialogs"),
DefaultDialogs = brackets.getModule("widgets/DefaultDialogs"),
Commands = brackets.getModule("command/Commands"),
CommandManager = brackets.getModule("command/CommandManager"),
StringUtils = brackets.getModule("utils/StringUtils");
var clientFilePath = ExtensionUtils.getModulePath(module, "client.js"),
clientName = "PhpClient",
_client = null,
evtHandler,
phpConfig = {
enablePhpTooling: true,
executablePath: "php",
memoryLimit: "4095M",
validateOnType: "false"
},
DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW = "debug.openPrefsInSplitView",
phpServerRunning = false,
serverCapabilities,
currentRootPath;
PreferencesManager.definePreference("php", "object", phpConfig, {
description: Strings.DESCRIPTION_PHP_TOOLING_CONFIGURATION
});
PreferencesManager.on("change", "php", function () {
var newPhpConfig = PreferencesManager.get("php");
if ((newPhpConfig["executablePath"] !== phpConfig["executablePath"])
|| (newPhpConfig["enablePhpTooling"] !== phpConfig["enablePhpTooling"])) {
phpConfig = newPhpConfig;
runPhpServer();
return;
}
phpConfig = newPhpConfig;
});
var handleProjectOpen = function (event, directory) {
if(serverCapabilities["workspace"] && serverCapabilities["workspace"]["workspaceFolders"]) {
_client.notifyProjectRootsChanged({
foldersAdded: [directory.fullPath],
foldersRemoved: [currentRootPath]
});
currentRootPath = directory.fullPath;
} else {
_client.restart({
rootPath: directory.fullPath
}).done(handlePostPhpServerStart);
}
};
function registerToolingProviders() {
var chProvider = new CodeHintsProvider(_client),
phProvider = new DefaultProviders.ParameterHintsProvider(_client),
lProvider = new DefaultProviders.LintingProvider(_client),
jdProvider = new DefaultProviders.JumpToDefProvider(_client);
JumpToDefManager.registerJumpToDefProvider(jdProvider, ["php"], 0);
CodeHintManager.registerHintProvider(chProvider, ["php"], 0);
ParameterHintManager.registerHintProvider(phProvider, ["php"], 0);
CodeInspection.register(["php"], {
name: Strings.PHP_DIAGNOSTICS,
scanFile: lProvider.getInspectionResults.bind(lProvider)
});
_client.addOnCodeInspection(lProvider.setInspectionResults.bind(lProvider));
}
function addEventHandlers() {
_client.addOnLogMessage(function () {});
_client.addOnShowMessage(function () {});
evtHandler = new DefaultEventHandlers.EventPropagationProvider(_client);
evtHandler.registerClientForEditorEvent();
if (phpConfig["validateOnType"] !== "false") {
_client.addOnDocumentChangeHandler(function () {
CodeInspection.requestRun(Strings.PHP_DIAGNOSTICS);
});
}
_client.addOnProjectOpenHandler(handleProjectOpen);
}
function validatePhpExecutable() {
var result = $.Deferred();
_client.sendCustomRequest({
messageType: "brackets",
type: "validatePhpExecutable",
params: phpConfig
}).done(result.resolve).fail(result.reject);
return result;
}
function showErrorPopUp(err) {
if (typeof (err) === "string") {
err = Strings[err];
} else {
err = StringUtils.format(Strings[err[0]], err[1]);
}
var Buttons = [
{ className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL,
text: Strings.CANCEL },
{ className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_DOWNLOAD,
text: Strings.OPEN_PREFERENNCES}
];
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.PHP_SERVER_ERROR_TITLE,
err,
Buttons
).done(function (id) {
if (id === Dialogs.DIALOG_BTN_DOWNLOAD) {
if (CommandManager.get(DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW)) {
CommandManager.execute(DEBUG_OPEN_PREFERENCES_IN_SPLIT_VIEW);
} else {
CommandManager.execute(Commands.CMD_OPEN_PREFERENCES);
}
}
});
}
function handlePostPhpServerStart() {
if (!phpServerRunning) {
phpServerRunning = true;
registerToolingProviders();
addEventHandlers();
EditorManager.off("activeEditorChange.php");
LanguageManager.off("languageModified.php");
}
evtHandler.handleActiveEditorChange(null, EditorManager.getActiveEditor());
currentRootPath = ProjectManager.getProjectRoot()._path;
setTimeout(function () {
CodeInspection.requestRun(Strings.PHP_DIAGNOSTICS);
}, 1500);
}
function runPhpServer() {
if (_client && phpConfig["enablePhpTooling"]) {
validatePhpExecutable()
.done(function () {
var startFunc = _client.start.bind(_client);
if (phpServerRunning) {
startFunc = _client.restart.bind(_client);
}
currentRootPath = ProjectManager.getProjectRoot()._path;
startFunc({
rootPath: currentRootPath
}).done(function (result) {
console.log("php Language Server started");
serverCapabilities = result.capabilities;
handlePostPhpServerStart();
});
}).fail(showErrorPopUp);
}
}
function activeEditorChangeHandler(event, current) {
if (current) {
var language = current.document.getLanguage();
if (language.getId() === "php") {
runPhpServer();
EditorManager.off("activeEditorChange.php");
LanguageManager.off("languageModified.php");
}
}
}
function languageModifiedHandler(event, language) {
if (language && language.getId() === "php") {
runPhpServer();
EditorManager.off("activeEditorChange.php");
LanguageManager.off("languageModified.php");
}
}
AppInit.appReady(function () {
LanguageTools.initiateToolingService(clientName, clientFilePath, ['php']).done(function (client) {
_client = client;
EditorManager.on("activeEditorChange.php", activeEditorChangeHandler);
LanguageManager.on("languageModified.php", languageModifiedHandler);
activeEditorChangeHandler(null, EditorManager.getActiveEditor());
});
});
//Only for Unit testing
exports.getClient = function() { return _client; };
});

View File

@ -0,0 +1,14 @@
{
"name": "php-Tooling",
"version": "1.0.0",
"description": "Advanced Tooling support for PHP",
"author": "niteskum",
"main": "main.js",
"scripts": {
"postinstall": "composer require felixfbecker/language-server && composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs"
},
"dependencies": {
"execa": "1.0.0",
"semver": "5.6.0"
}
}

View File

@ -0,0 +1,75 @@
{
"$GLOBALS": {
"description": "An associative array containing references to all variables which are currently defined in the global scope of the script. The variable names are the keys of the array.",
"type": "array"
},
"$_SERVER": {
"description": "$_SERVER is an array containing information such as headers, paths, and script locations. The entries in this array are created by the web server. There is no guarantee that every web server will provide any of these; servers may omit some, or provide others not listed here. That said, a large number of these variables are accounted for in the CGI/1.1 specification, so you should be able to expect those.",
"type": "array"
},
"$_GET": {
"description": "An associative array of variables passed to the current script via the URL parameters.",
"type": "array"
},
"$_POST": {
"description": "An associative array of variables passed to the current script via the HTTP POST method.",
"type": "array"
},
"$_FILES": {
"description": "An associative array of items uploaded to the current script via the HTTP POST method.",
"type": "array"
},
"$_REQUEST": {
"description": "An associative array that by default contains the contents of $_GET, $_POST and $_COOKIE.",
"type": "array"
},
"$_SESSION": {
"description": "An associative array containing session variables available to the current script. See the Session functions documentation for more information on how this is used.",
"type": "array"
},
"$_ENV": {
"description": "An associative array of variables passed to the current script via the environment method. \r\n\r\nThese variables are imported into PHP\"s global namespace from the environment under which the PHP parser is running. Many are provided by the shell under which PHP is running and different systems are likely running different kinds of shells, a definitive list is impossible. Please see your shell\"s documentation for a list of defined environment variables. \r\n\r\nOther environment variables include the CGI variables, placed there regardless of whether PHP is running as a server module or CGI processor.",
"type": "array"
},
"$_COOKIE": {
"description": "An associative array of variables passed to the current script via HTTP Cookies.",
"type": "array"
},
"$php_errormsg": {
"description": "$php_errormsg is a variable containing the text of the last error message generated by PHP. This variable will only be available within the scope in which the error occurred, and only if the track_errors configuration option is turned on (it defaults to off).",
"type": "array"
},
"$HTTP_RAW_POST_DATA": {
"description": "$HTTP_RAW_POST_DATA contains the raw POST data. See always_populate_raw_post_data",
"type": "array"
},
"$http_response_header": {
"description": "The $http_response_header array is similar to the get_headers() function. When using the HTTP wrapper, $http_response_header will be populated with the HTTP response headers. $http_response_header will be created in the local scope.",
"type": "array"
},
"$argc": {
"description": "Contains the number of arguments passed to the current script when running from the command line.",
"type": "array"
},
"$argv": {
"description": "Contains an array of all the arguments passed to the script when running from the command line.",
"type": "array"
},
"$this": {
"description": "Refers to the current object",
"type": "array"
},
"parent": {
"description": "",
"type": ""
},
"self": {
"description": "",
"type": ""
},
"_destruct": {
"description": "",
"type": ""
}
}

View File

@ -0,0 +1,2 @@
<?php
echo "Hello World!"

View File

@ -0,0 +1,30 @@
<?php
$x = 75;
$x = 34;
namespace test;
abstract class TestCase extends testA
{
use testA;
}
$A11 = 23;
$A12 = 34;
$A13 = 45;
$A1
fo
$
function watchparameterhint() {
echo "Hello World!";
}
$A11()
fopen("",)
watchparameterhint()
?>

View File

@ -0,0 +1,10 @@
<?php
namespace test;
class testA
{
protected $B = [
'A1', 'A2'
];
}

View File

@ -0,0 +1,499 @@
/*
* 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.
*
*/
/*global describe, runs, beforeEach, it, expect, waitsFor, waitsForDone, beforeFirst, afterLast */
define(function (require, exports, module) {
'use strict';
var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
Strings = brackets.getModule("strings"),
FileUtils = brackets.getModule("file/FileUtils"),
StringUtils = brackets.getModule("utils/StringUtils");
var extensionRequire,
phpToolingExtension,
testWindow,
$,
PreferencesManager,
CodeInspection,
DefaultProviders,
CodeHintsProvider,
EditorManager,
testEditor,
testFolder = FileUtils.getNativeModuleDirectoryPath(module) + "/unittest-files/",
testFile1 = "test1.php",
testFile2 = "test2.php";
describe("PhpTooling", function () {
beforeFirst(function () {
// Create a new window that will be shared by ALL tests in this spec.
SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
testWindow = w;
$ = testWindow.$;
var brackets = testWindow.brackets;
extensionRequire = brackets.test.ExtensionLoader.getRequireContextForExtension("PhpTooling");
phpToolingExtension = extensionRequire("main");
});
});
afterLast(function () {
waitsForDone(phpToolingExtension.getClient().stop(), "stoping php server");
SpecRunnerUtils.closeTestWindow();
testWindow = null;
});
beforeEach(function () {
EditorManager = testWindow.brackets.test.EditorManager;
PreferencesManager = testWindow.brackets.test.PreferencesManager;
CodeInspection = testWindow.brackets.test.CodeInspection;
CodeInspection.toggleEnabled(true);
DefaultProviders = testWindow.brackets.getModule("languageTools/DefaultProviders");
CodeHintsProvider = extensionRequire("CodeHintsProvider");
});
/**
* Does a busy wait for a given number of milliseconds
* @param {Number} milliSeconds - number of milliSeconds to wait
*/
function waitForMilliSeconds(milliSeconds) {
var flag = false;
setTimeout(function () {
flag = true;
}, milliSeconds);
waitsFor(function () {
return flag;
}, "This should not fail. Please check the timeout values.",
milliSeconds + 10); // We give 10 milliSeconds as grace period
}
/**
* Check the presence of a Button in Error Prompt
* @param {String} btnId - "CANCEL" or "OPEN"
*/
function checkPopUpButton(clickbtnId) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
btn = errorPopUp.find('.dialog-button');
// Test if the update bar button has been displayed.
expect(btn.length).toBe(2);
if (clickbtnId) {
clickButton(clickbtnId);
}
}
/**
* Check the presence of a Button in Error Prompt
* @param {String} btnId - Button OPEN or Cancel Button
*/
function clickButton(btnId) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
btn = errorPopUp.find('.dialog-button'),
openBtn,
cancelBtn,
clickBtn;
if (btn[0].classList.contains("primary")) {
openBtn = btn[0];
cancelBtn = btn[1];
}
if (btn[1].classList.contains("primary")) {
openBtn = btn[1];
cancelBtn = btn[0];
}
clickBtn = cancelBtn;
if(btnId === "OPEN") {
clickBtn = openBtn;
}
if(clickBtn) {
clickBtn.click();
waitForMilliSeconds(3000);
runs(function() {
expect(doc.find(".error-dialog.instance").length).toBe(0);
});
}
}
/**
* Check the presence of Error Prompt String on Brackets Window
* @param {String} title - Title String Which will be matched with Update Bar heading.
* @param {String} description - description String Which will be matched with Update Bar description.
*/
function checkPopUpString(title, titleDescription) {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
heading = errorPopUp.find('.dialog-title'),
description = errorPopUp.find('.dialog-message');
// Test if the update bar has been displayed.
//expect(errorPopUp.length).toBe(1);
if (title) {
expect(heading.text()).toBe(title);
}
if (titleDescription) {
expect(description.text()).toBe(titleDescription);
}
}
function toggleDiagnosisResults(visible) {
var doc = $(testWindow.document),
problemsPanel = doc.find("#problems-panel"),
statusInspection = $("#status-inspection");
statusInspection.triggerHandler("click");
expect(problemsPanel.is(":visible")).toBe(visible);
}
/**
* Wait for the editor to change positions, such as after a jump to
* definition has been triggered. Will timeout after 3 seconds
*
* @param {{line:number, ch:number}} oldLocation - the original line/col
* @param {Function} callback - the callback to apply once the editor has changed position
*/
function _waitForJump(jumpPromise, callback) {
var cursor = null,
complete = false;
jumpPromise.done(function () {
complete = true;
});
waitsFor(function () {
var activeEditor = EditorManager.getActiveEditor();
cursor = activeEditor.getCursorPos();
return complete;
}, "Expected jump did not occur", 3000);
runs(function () { callback(cursor); });
}
/*
* Expect a given list of hints to be present in a given hint
* response object
*
* @param {Object + jQuery.Deferred} hintObj - a hint response object,
* possibly deferred
* @param {Array.<string>} expectedHints - a list of hints that should be
* present in the hint response
*/
function expecthintsPresent(expectedHints) {
var hintObj = (new CodeHintsProvider.CodeHintsProvider(phpToolingExtension.getClient())).getHints(null);
_waitForHints(hintObj, function (hintList) {
expect(hintList).toBeTruthy();
expectedHints.forEach(function (expectedHint) {
expect(_indexOf(hintList, expectedHint)).not.toBe(-1);
});
});
}
/*
* Return the index at which hint occurs in hintList
*
* @param {Array.<Object>} hintList - the list of hints
* @param {string} hint - the hint to search for
* @return {number} - the index into hintList at which the hint occurs,
* or -1 if it does not
*/
function _indexOf(hintList, hint) {
var index = -1,
counter = 0;
for (counter; counter < hintList.length; counter++) {
if (hintList[counter].data("token").label === hint) {
index = counter;
break;
}
}
return index;
}
/*
* Wait for a hint response object to resolve, then apply a callback
* to the result
*
* @param {Object + jQuery.Deferred} hintObj - a hint response object,
* possibly deferred
* @param {Function} callback - the callback to apply to the resolved
* hint response object
*/
function _waitForHints(hintObj, callback) {
var complete = false,
hintList = null;
if (hintObj.hasOwnProperty("hints")) {
complete = true;
hintList = hintObj.hints;
} else {
hintObj.done(function (obj) {
complete = true;
hintList = obj.hints;
});
}
waitsFor(function () {
return complete;
}, "Expected hints did not resolve", 3000);
runs(function () { callback(hintList); });
}
/**
* Show a function hint based on the code at the cursor. Verify the
* hint matches the passed in value.
*
* @param {Array<{name: string, type: string, isOptional: boolean}>}
* expectedParams - array of records, where each element of the array
* describes a function parameter. If null, then no hint is expected.
* @param {number} expectedParameter - the parameter at cursor.
*/
function expectParameterHint(expectedParams, expectedParameter) {
var requestStatus = null;
var request,
complete = false;
runs(function () {
request = (new DefaultProviders.ParameterHintsProvider(phpToolingExtension.getClient()))
.getParameterHints();
request.done(function (status) {
complete = true;
requestStatus = status;
}).fail(function(){
complete = true;
});
});
waitsFor(function () {
return complete;
}, "Expected Parameter hints did not resolve", 3000);
if (expectedParams === null) {
expect(requestStatus).toBe(null);
return;
}
function expectHint(hint) {
var params = hint.parameters,
n = params.length,
i;
// compare params to expected params
expect(params.length).toBe(expectedParams.length);
expect(hint.currentIndex).toBe(expectedParameter);
for (i = 0; i < n; i++) {
expect(params[i].label).toBe(expectedParams[i]);
}
}
runs(function() {
expectHint(requestStatus);
});
}
/**
* Trigger a jump to definition, and verify that the editor jumped to
* the expected location. The new location is the variable definition
* or function definition of the variable or function at the current
* cursor location. Jumping to the new location will cause a new editor
* to be opened or open an existing editor.
*
* @param {{line:number, ch:number, file:string}} expectedLocation - the
* line, column, and optionally the new file the editor should jump to. If the
* editor is expected to stay in the same file, then file may be omitted.
*/
function editorJumped(expectedLocation) {
var jumpPromise = (new DefaultProviders.JumpToDefProvider(phpToolingExtension.getClient())).doJumpToDef();
_waitForJump(jumpPromise, function (newCursor) {
expect(newCursor.line).toBe(expectedLocation.line);
expect(newCursor.ch).toBe(expectedLocation.ch);
if (expectedLocation.file) {
var activeEditor = EditorManager.getActiveEditor();
expect(activeEditor.document.file.name).toBe(expectedLocation.file);
}
});
}
/**
* Check the presence of Error Prompt on Brackets Window
*/
function checkErrorPopUp() {
var doc = $(testWindow.document),
errorPopUp = doc.find(".error-dialog.instance"),
errorPopUpHeader = errorPopUp.find(".modal-header"),
errorPopUpBody = errorPopUp.find(".modal-body"),
errorPopUpFooter = errorPopUp.find(".modal-footer"),
errorPopUpPresent = false;
runs(function () {
expect(errorPopUp.length).toBe(1);
expect(errorPopUpHeader).not.toBeNull();
expect(errorPopUpBody).not.toBeNull();
expect(errorPopUpFooter).not.toBeNull();
});
if (errorPopUp && errorPopUp.length > 0) {
errorPopUpPresent = true;
}
return errorPopUpPresent;
}
it("phpTooling Exiension should be loaded Successfully", function () {
waitForMilliSeconds(5000);
runs(function () {
expect(phpToolingExtension).not.toBeNull();
});
});
it("should attempt to start php server and fail due to lower version of php", function () {
var phpExecutable = testWindow.brackets.platform === "mac" ? "/mac/invalidphp" : "/win/invalidphp";
PreferencesManager.set("php", {
"executablePath": testFolder + phpExecutable
}, {
locations: {scope: "session"}
});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE,
StringUtils.format(Strings.PHP_UNSUPPORTED_VERSION, "5.6.30"));
checkPopUpButton("CANCEL");
});
});
it("should attempt to start php server and fail due to invaild executable", function () {
PreferencesManager.set("php", {"executablePath": "/invalidPath/php"}, {locations: {scope: "session"}});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_EXECUTABLE_NOT_FOUND);
checkPopUpButton("CANCEL");
});
});
it("should attempt to start php server and fail due to invaild memory limit in prefs settings", function () {
PreferencesManager.set("php", {"memoryLimit": "invalidValue"}, {locations: {scope: "session"}});
waitForMilliSeconds(5000);
runs(function () {
checkErrorPopUp();
checkPopUpString(Strings.PHP_SERVER_ERROR_TITLE, Strings.PHP_SERVER_MEMORY_LIMIT_INVALID);
checkPopUpButton("CANCEL");
});
runs(function () {
SpecRunnerUtils.loadProjectInTestWindow(testFolder + "test");
});
});
it("should attempt to start php server and success", function () {
PreferencesManager.set("php", {"memoryLimit": "4095M"}, {locations: {scope: "session"}});
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile1]), "open test file: " + testFile1);
waitForMilliSeconds(5000);
runs(function () {
toggleDiagnosisResults(false);
toggleDiagnosisResults(true);
});
});
it("should filter hints by query", function () {
waitsForDone(SpecRunnerUtils.openProjectFiles([testFile2]), "open test file: " + testFile2);
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 15, ch: 3 });
expecthintsPresent(["$A11", "$A12", "$A13"]);
});
});
it("should show inbuilt functions in hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 17, ch: 2 });
expecthintsPresent(["fopen", "for", "foreach"]);
});
});
it("should show static global variables in hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 20, ch: 1 });
expecthintsPresent(["$_COOKIE", "$_ENV"]);
});
});
it("should not show parameter hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 25, ch: 5 });
expectParameterHint(null);
});
});
it("should show no parameter as a hint", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 27, ch: 19 });
expectParameterHint([], 0);
});
});
it("should show parameters hints", function () {
runs(function() {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos({ line: 26, ch: 9 });
expectParameterHint([
"string $filename",
"string $mode",
"bool $use_include_path = null",
"resource $context = null"], 1);
});
});
it("should jump to earlier defined variable", function () {
var start = { line: 4, ch: 2 };
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
editorJumped({line: 2, ch: 0});
});
});
it("should jump to class declared in other module file", function () {
var start = { line: 9, ch: 11 };
runs(function () {
testEditor = EditorManager.getActiveEditor();
testEditor.setCursorPos(start);
editorJumped({line: 4, ch: 0, file: "test3.php"});
});
});
});
});

View File

@ -872,11 +872,22 @@ define({
"DESCRIPTION_AUTO_UPDATE" : "Enable/disable Brackets Auto-update",
"AUTOUPDATE_ERROR" : "Error!",
"AUTOUPDATE_IN_PROGRESS" : "An update is already in progress.",
"NUMBER_WITH_PERCENTAGE" : "{0}%",
// Strings for Related Files
"CMD_FIND_RELATED_FILES" : "Find Related Files",
///String for Php Tooling Extensions
"PHP_VERSION_INVALID" : "Error parsing PHP version. Please check the output of the “php version” command.",
"PHP_UNSUPPORTED_VERSION" : "Install PHP7 runtime for enabling PHP-related tooling such as Code Hints, Parameter Hints, Jump To Definition and more. Version found: {0}",
"PHP_EXECUTABLE_NOT_FOUND" : "PHP runtime not found. Install PHP7 runtime and set the path to system PATH or executablePath in php Preferences appropriately.",
"PHP_PROCESS_SPAWN_ERROR" : "Error code {0} encountered while starting the PHP process.",
"PHP_SERVER_ERROR_TITLE" : "Error",
"PHP_SERVER_MEMORY_LIMIT_INVALID" : "The memory limit you provided is invalid. Review the PHP preferences to set the correct value.",
"DESCRIPTION_PHP_TOOLING_CONFIGURATION" : "PHP Tooling default configuration settings",
"OPEN_PREFERENNCES" : "Open Preferences",
"PHP_DIAGNOSTICS" : "Diagnostics",
//Strings for LanguageTools Preferences
LANGUAGE_TOOLS_PREFERENCES : "Preferences for Language Tools"
});