diff --git a/src/base-config/keyboard.json b/src/base-config/keyboard.json index 07611fea90c..5d7ea339515 100644 --- a/src/base-config/keyboard.json +++ b/src/base-config/keyboard.json @@ -162,6 +162,9 @@ "cmd.findInFiles": [ "Ctrl-Shift-F" ], + "cmd.findAllReferences": [ + "Shift-F12" + ], "cmd.replaceInFiles": [ { "key": "Ctrl-Shift-H" diff --git a/src/brackets.js b/src/brackets.js index 4365249ef05..a9935b1589b 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -141,6 +141,7 @@ define(function (require, exports, module) { //load language features require("features/ParameterHintsManager"); require("features/JumpToDefManager"); + require("features/FindReferencesManager"); // Load modules that self-register and just need to get included in the main project require("command/DefaultMenus"); diff --git a/src/command/Commands.js b/src/command/Commands.js index e5811dd229c..d0f0087676b 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -97,6 +97,7 @@ define(function (require, exports, module) { exports.CMD_REPLACE = "cmd.replace"; // FindReplace.js _replace() exports.CMD_REPLACE_IN_FILES = "cmd.replaceInFiles"; // FindInFilesUI.js _showReplaceBar() exports.CMD_REPLACE_IN_SUBTREE = "cmd.replaceInSubtree"; // FindInFilesUI.js _showReplaceBarForSubtree() + exports.CMD_FIND_ALL_REFERENCES = "cmd.findAllReferences"; // findReferencesManager.js _openReferencesPanel() // VIEW exports.CMD_THEMES_OPEN_SETTINGS = "view.themesOpenSetting"; // MenuCommands.js Settings.open() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 912d9eaa306..86e6776d1fd 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -136,6 +136,7 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.CMD_SKIP_CURRENT_MATCH); menu.addMenuDivider(); menu.addMenuItem(Commands.CMD_FIND_IN_FILES); + menu.addMenuItem(Commands.CMD_FIND_ALL_REFERENCES); menu.addMenuDivider(); menu.addMenuItem(Commands.CMD_REPLACE); menu.addMenuItem(Commands.CMD_REPLACE_IN_FILES); @@ -283,6 +284,7 @@ define(function (require, exports, module) { // editor_cmenu.addMenuItem(Commands.NAVIGATE_JUMPTO_DEFINITION); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_DOCS); + editor_cmenu.addMenuItem(Commands.CMD_FIND_ALL_REFERENCES); editor_cmenu.addMenuDivider(); editor_cmenu.addMenuItem(Commands.EDIT_CUT); editor_cmenu.addMenuItem(Commands.EDIT_COPY); diff --git a/src/extensions/default/PhpTooling/main.js b/src/extensions/default/PhpTooling/main.js index a2de7502d50..afd2059d084 100755 --- a/src/extensions/default/PhpTooling/main.js +++ b/src/extensions/default/PhpTooling/main.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { QuickOpen = brackets.getModule("search/QuickOpen"), ParameterHintManager = brackets.getModule("features/ParameterHintsManager"), JumpToDefManager = brackets.getModule("features/JumpToDefManager"), + FindReferencesManager = brackets.getModule("features/FindReferencesManager"), CodeInspection = brackets.getModule("language/CodeInspection"), DefaultProviders = brackets.getModule("languageTools/DefaultProviders"), CodeHintsProvider = require("CodeHintsProvider").CodeHintsProvider, @@ -65,7 +66,8 @@ define(function (require, exports, module) { lProvider, jdProvider, dSymProvider, - pSymProvider; + pSymProvider, + refProvider; PreferencesManager.definePreference("php", "object", phpConfig, { description: Strings.DESCRIPTION_PHP_TOOLING_CONFIGURATION @@ -108,10 +110,12 @@ define(function (require, exports, module) { jdProvider = new DefaultProviders.JumpToDefProvider(_client); dSymProvider = new SymbolProviders.DocumentSymbolsProvider(_client); pSymProvider = new SymbolProviders.ProjectSymbolsProvider(_client); + refProvider = new DefaultProviders.ReferencesProvider(_client); JumpToDefManager.registerJumpToDefProvider(jdProvider, ["php"], 0); CodeHintManager.registerHintProvider(chProvider, ["php"], 0); ParameterHintManager.registerHintProvider(phProvider, ["php"], 0); + FindReferencesManager.registerFindReferencesProvider(refProvider, ["php"], 0); CodeInspection.register(["php"], { name: "", scanFileAsync: lProvider.getInspectionResultsAsync.bind(lProvider) diff --git a/src/extensions/default/PhpTooling/unittest-files/test/test2.php b/src/extensions/default/PhpTooling/unittest-files/test/test2.php index 16651124644..5080a3f3158 100644 --- a/src/extensions/default/PhpTooling/unittest-files/test/test2.php +++ b/src/extensions/default/PhpTooling/unittest-files/test/test2.php @@ -26,5 +26,22 @@ function watchparameterhint() { $A11() fopen("",) watchparameterhint() + + +function watchReferences() { + echo "Hello World!"; +} + +watchReferences(); + +watchReferences(); + + +function ReferencesInMultipleFile() { + echo "Hello World!"; +} + +ReferencesInMultipleFile(); +ReferencesInMultipleFile(); ?> diff --git a/src/extensions/default/PhpTooling/unittest-files/test/test3.php b/src/extensions/default/PhpTooling/unittest-files/test/test3.php index 70981b54d1f..7dc1d365bd2 100644 --- a/src/extensions/default/PhpTooling/unittest-files/test/test3.php +++ b/src/extensions/default/PhpTooling/unittest-files/test/test3.php @@ -7,4 +7,6 @@ class testA protected $B = [ 'A1', 'A2' ]; -} \ No newline at end of file +} + +ReferencesInMultipleFile(); \ No newline at end of file diff --git a/src/extensions/default/PhpTooling/unittests.js b/src/extensions/default/PhpTooling/unittests.js index 3724743b5e7..53b915227da 100644 --- a/src/extensions/default/PhpTooling/unittests.js +++ b/src/extensions/default/PhpTooling/unittests.js @@ -398,6 +398,40 @@ define(function (require, exports, module) { } + function expectReferences(referencesExpected) { + var refPromise, + results = null, + complete = false; + runs(function () { + refPromise = (new DefaultProviders.ReferencesProvider(phpToolingExtension.getClient())).getReferences(); + refPromise.done(function (resp) { + complete = true; + results = resp; + }).fail(function(){ + complete = true; + }); + }); + + waitsFor(function () { + return complete; + }, "Expected Reference Promise did not resolve", 3000); + + if(referencesExpected === null) { + expect(results).toBeNull(); + return; + } + + runs(function() { + expect(results.numFiles).toBe(referencesExpected.numFiles); + expect(results.numMatches).toBe(referencesExpected.numMatches); + expect(results.allResultsAvailable).toBe(referencesExpected.allResultsAvailable); + expect(results.results).not.toBeNull(); + for(var key in results.keys) { + expect(results.results.key).toBe(referencesExpected.results.key); + } + }); + } + /** * Check the presence of Error Prompt on Brackets Window */ @@ -533,6 +567,112 @@ define(function (require, exports, module) { }); }); + it("should not show any references", function () { + var start = { line: 6, ch: 4 }; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + expectReferences(null); + }); + }); + + it("should show reference present in single file", function () { + var start = { line: 22, ch: 18 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 27, ch: 0}, + end: {line: 27, ch: 18}, + line: "watchparameterhint()" + } + ] + }; + expectReferences({ + numFiles: 1, + numMatches: 1, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php"], + results: results + }); + }); + }); + + it("should show references present in single file", function () { + var start = { line: 34, ch: 8 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 34, ch: 0}, + end: {line: 34, ch: 17}, + line: "watchReferences();" + }, + { + start: {line: 36, ch: 0}, + end: {line: 36, ch: 17}, + line: "watchReferences();" + } + ] + }; + expectReferences({ + numFiles: 1, + numMatches: 2, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php"], + results: results + }); + }); + }); + + it("should show references present in multiple files", function () { + var start = { line: 39, ch: 21 }, + results = {}; + + runs(function () { + testEditor = EditorManager.getActiveEditor(); + testEditor.setCursorPos(start); + results[testFolder + "test/test2.php"] = {matches: [ + { + start: {line: 34, ch: 0}, + end: {line: 34, ch: 26}, + line: "watchReferences();" + }, + { + start: {line: 36, ch: 0}, + end: {line: 36, ch: 26}, + line: "watchReferences();" + } + ] + }; + results[testFolder + "test/test3.php"] = {matches: [ + { + start: {line: 11, ch: 0}, + end: {line: 11, ch: 26}, + line: "watchReferences();" + } + ] + }; + expectReferences({ + numFiles: 2, + numMatches: 3, + allResultsAvailable: true, + queryInfo: "watchparameterhint", + keys: [testFolder + "test/test2.php", testFolder + "test/test3.php"], + results: results + }); + }); + }); + it("should jump to earlier defined variable", function () { var start = { line: 4, ch: 2 }; diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js new file mode 100644 index 00000000000..3ae4c4b3cbd --- /dev/null +++ b/src/features/FindReferencesManager.js @@ -0,0 +1,148 @@ +/* + * 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 AppInit = require("utils/AppInit"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + EditorManager = require("editor/EditorManager"), + ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler, + SearchResultsView = require("search/SearchResultsView").SearchResultsView, + SearchModel = require("search/SearchModel").SearchModel, + Strings = require("strings"); + + var _providerRegistrationHandler = new ProviderRegistrationHandler(), + registerFindReferencesProvider = _providerRegistrationHandler.registerProvider.bind( + _providerRegistrationHandler + ), + removeFindReferencesProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + + var searchModel = new SearchModel(), + _resultsView; + + function _getReferences(provider, hostEditor, pos) { + var result = new $.Deferred(); + + if(!provider) { + return result.reject(); + } + + provider.getReferences(hostEditor, pos) + .done(function (rcvdObj) { + + searchModel.results = rcvdObj.results; + searchModel.numFiles = rcvdObj.numFiles; + searchModel.numMatches = rcvdObj.numMatches; + searchModel.allResultsAvailable = true; + searchModel.setQueryInfo({query: rcvdObj.queryInfo, caseSensitive: true, isRegExp: false}); + result.resolve(); + }).fail(function (){ + result.reject(); + }); + return result.promise(); + + } + + function _openReferencesPanel() { + var editor = EditorManager.getActiveEditor(), + pos = editor ? editor.getCursorPos() : null, + referencesPromise, + result = new $.Deferred(), + errorMsg = Strings.REFERENCES_NO_RESULTS, + referencesProvider; + + var language = editor.getLanguageForSelection(), + enabledProviders = _providerRegistrationHandler.getProvidersForLanguageId(language.getId()); + + enabledProviders.some(function (item, index) { + if (item.provider.hasReferences(editor)) { + referencesProvider = item.provider; + return true; + } + }); + + referencesPromise = _getReferences(referencesProvider, editor, pos); + + // If one of them will provide a widget, show it inline once ready + if (referencesPromise) { + referencesPromise.done(function () { + if(_resultsView) { + _resultsView.open(); + } + }).fail(function () { + if(_resultsView) { + _resultsView.close(); + } + editor.displayErrorMessageAtCursor(errorMsg); + result.reject(); + }); + } else { + if(_resultsView) { + _resultsView.close(); + } + editor.displayErrorMessageAtCursor(errorMsg); + result.reject(); + } + + return result.promise(); + } + + /** + * @private + * Clears any previous search information, removing update listeners and clearing the model. + */ + function _clearSearch() { + searchModel.clear(); + } + + AppInit.htmlReady(function () { + _resultsView = new SearchResultsView( + searchModel, + "reference-in-files-results", + "reference-in-files.results", + "reference" + ); + if(_resultsView) { + _resultsView + .on("close", function () { + _clearSearch(); + }) + .on("getNextPage", function () { + if (searchModel.hasResults()) { + _resultsView.showNextPage(); + } + }) + .on("getLastPage", function () { + if (searchModel.hasResults()) { + _resultsView.showLastPage(); + } + }); + } + }); + CommandManager.register(Strings.FIND_ALL_REFERENCES, Commands.CMD_FIND_ALL_REFERENCES, _openReferencesPanel); + + exports.registerFindReferencesProvider = registerFindReferencesProvider; + exports.removeFindReferencesProvider = removeFindReferencesProvider; +}); diff --git a/src/languageTools/DefaultProviders.js b/src/languageTools/DefaultProviders.js index fbeb67e36ec..47761ac0aa9 100644 --- a/src/languageTools/DefaultProviders.js +++ b/src/languageTools/DefaultProviders.js @@ -30,6 +30,7 @@ define(function (require, exports, module) { var _ = brackets.getModule("thirdparty/lodash"); var EditorManager = require('editor/EditorManager'), + DocumentManager = require('document/DocumentManager'), ExtensionUtils = require("utils/ExtensionUtils"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), @@ -404,8 +405,96 @@ define(function (require, exports, module) { return this._results.get(filePath); }; + function serverRespToSearchModelFormat(msgObj) { + var referenceModel = {}, + result = $.Deferred(); + + if(!(msgObj && msgObj.length && msgObj.cursorPos)) { + return result.reject(); + } + referenceModel.results = {}; + referenceModel.numFiles = 0; + var fulfilled = 0; + msgObj.forEach((element, i) => { + var filePath = PathConverters.uriToPath(element.uri); + DocumentManager.getDocumentForPath(filePath) + .done(function(doc) { + var startRange = {line: element.range.start.line, ch: element.range.start.character}; + var endRange = {line: element.range.end.line, ch: element.range.end.character}; + var match = { + start: startRange, + end: endRange, + highlightOffset: 0, + line: doc.getLine(element.range.start.line) + }; + if(!referenceModel.results[filePath]) { + referenceModel.numFiles = referenceModel.numFiles + 1; + referenceModel.results[filePath] = {"matches": []}; + } + if(!referenceModel.queryInfo || msgObj.cursorPos.line === startRange.line) { + referenceModel.queryInfo = doc.getRange(startRange, endRange); + } + referenceModel.results[filePath]["matches"].push(match); + }).always(function() { + fulfilled++; + if(fulfilled === msgObj.length) { + referenceModel.numMatches = msgObj.length; + referenceModel.allResultsAvailable = true; + result.resolve(referenceModel); + } + }); + }); + return result.promise(); + } + + function ReferencesProvider(client) { + this.client = client; + } + + ReferencesProvider.prototype.hasReferences = function() { + if (!this.client) { + return false; + } + + var serverCapabilities = this.client.getServerCapabilities(); + if (!serverCapabilities || !serverCapabilities.referencesProvider) { + return false; + } + + return true; + }; + + ReferencesProvider.prototype.getReferences = function(hostEditor, curPos) { + var editor = hostEditor || EditorManager.getActiveEditor(), + pos = curPos || editor ? editor.getCursorPos() : null, + docPath = editor.document.file._path, + result = $.Deferred(); + + if (this.client) { + this.client.findReferences({ + filePath: docPath, + cursorPos: pos + }).done(function(msgObj){ + if(msgObj && msgObj.length) { + msgObj.cursorPos = pos; + serverRespToSearchModelFormat(msgObj) + .done(result.resolve) + .fail(result.reject); + } else { + result.reject(); + } + }).fail(function(){ + result.reject(); + }); + return result.promise(); + } + return result.reject(); + }; + exports.CodeHintsProvider = CodeHintsProvider; exports.ParameterHintsProvider = ParameterHintsProvider; exports.JumpToDefProvider = JumpToDefProvider; exports.LintingProvider = LintingProvider; + exports.ReferencesProvider = ReferencesProvider; + exports.serverRespToSearchModelFormat = serverRespToSearchModelFormat; }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index f4874acbd6c..2668105fa05 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -890,6 +890,12 @@ define({ //Strings for LanguageTools Preferences "LANGUAGE_TOOLS_PREFERENCES" : "Preferences for Language Tools", + + "FIND_ALL_REFERENCES" : "Find All References", + "REFERENCES_IN_FILES" : "references", + "REFERENCE_IN_FILES" : "reference", + "REFERENCES_NO_RESULTS" : "No References available for current cursor position", + "CMD_FIND_DOCUMENT_SYMBOLS" : "Find Document Symbols", "CMD_FIND_PROJECT_SYMBOLS" : "Find Project Symbols" }); diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js index 13eb92790a0..1822926a26b 100644 --- a/src/search/SearchResultsView.js +++ b/src/search/SearchResultsView.js @@ -72,14 +72,16 @@ define(function (require, exports, module) { * @param {SearchModel} model The model that this view is showing. * @param {string} panelID The CSS ID to use for the panel. * @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel(). + * @param {string} type type to identify if it is reference search or string match serach */ - function SearchResultsView(model, panelID, panelName) { + function SearchResultsView(model, panelID, panelName, type) { var panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100); this._$summary = this._panel.$panel.find(".title"); this._$table = this._panel.$panel.find(".table-container"); this._model = model; + this._searchResultsType = type; } EventDispatcher.makeEventDispatcher(SearchResultsView.prototype); @@ -116,6 +118,9 @@ define(function (require, exports, module) { /** @type {number} The ID we use for timeouts when handling model changes. */ SearchResultsView.prototype._timeoutID = null; + /** @type {string} The Id we use to check if it is reference search or match search */ + SearchResultsView.prototype._searchResultsType = null; + /** * @private * Handles when model changes. Updates the view, buffering changes if necessary so as not to churn too much. @@ -344,9 +349,14 @@ define(function (require, exports, module) { SearchResultsView.prototype._showSummary = function () { var count = this._model.countFilesMatches(), lastIndex = this._getLastIndex(count.matches), + typeStr = (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, filesStr, summary; + if(this._searchResultsType === "reference") { + typeStr = (count.matches > 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES; + } + filesStr = StringUtils.format( Strings.FIND_NUM_FILES, count.files, @@ -358,7 +368,7 @@ define(function (require, exports, module) { Strings.FIND_TITLE_SUMMARY, this._model.exceedsMaximum ? Strings.FIND_IN_FILES_MORE_THAN : "", String(count.matches), - (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH, + typeStr, filesStr );