diff --git a/src/JSUtils/MessageIds.js b/src/JSUtils/MessageIds.js index 1d07ce36bcf..c154e4225f2 100644 --- a/src/JSUtils/MessageIds.js +++ b/src/JSUtils/MessageIds.js @@ -37,6 +37,7 @@ define(function (require, exports, module) { TERN_INFERENCE_TIMEDOUT = "InferenceTimedOut", SET_CONFIG = "SetConfig", TERN_UPDATE_DIRTY_FILE = "UpdateDirtyFileEntry", + TERN_REFS = "getRefs", TERN_CLEAR_DIRTY_FILES_LIST = "ClearDirtyFilesList"; // Message parameter constants @@ -62,6 +63,7 @@ define(function (require, exports, module) { exports.SET_CONFIG = SET_CONFIG; exports.TERN_UPDATE_DIRTY_FILE = TERN_UPDATE_DIRTY_FILE; exports.TERN_CLEAR_DIRTY_FILES_LIST = TERN_CLEAR_DIRTY_FILES_LIST; + exports.TERN_REFS = TERN_REFS; }); diff --git a/src/JSUtils/ScopeManager.js b/src/JSUtils/ScopeManager.js index c70eb4c98a0..0b1c06f5ac0 100644 --- a/src/JSUtils/ScopeManager.js +++ b/src/JSUtils/ScopeManager.js @@ -377,7 +377,28 @@ define(function (require, exports, module) { return text; } + /** + * Handle the response from the tern node domain when + * it responds with the references + * + * @param response - the response from the node domain + */ + function handleRename(response) { + + if (response.error) { + EditorManager.getActiveEditor().displayErrorMessageAtCursor(response.error); + return; + } + + var file = response.file, + offset = response.offset; + var $deferredFindRefs = getPendingRequest(file, offset, MessageIds.TERN_REFS); + + if ($deferredFindRefs) { + $deferredFindRefs.resolveWith(null, [response]); + } + } /** * Request Jump-To-Definition from Tern. @@ -390,10 +411,12 @@ define(function (require, exports, module) { */ function requestJumptoDef(session, document, offset) { var path = document.file.fullPath, - fileInfo = {type: MessageIds.TERN_FILE_INFO_TYPE_FULL, + fileInfo = { + type: MessageIds.TERN_FILE_INFO_TYPE_FULL, name: path, offsetLines: 0, - text: filterText(session.getJavascriptText())}; + text: filterText(session.getJavascriptText()) + }; var ternPromise = getJumptoDef(fileInfo, offset); @@ -1091,6 +1114,8 @@ define(function (require, exports, module) { handleTernGetFile(response); } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { handleJumptoDef(response); + } else if (type === MessageIds.TERN_REFS) { + handleRename(response); } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { handlePrimePumpCompletion(response); } else if (type === MessageIds.TERN_GET_GUESSES_MSG) { @@ -1557,5 +1582,8 @@ define(function (require, exports, module) { exports.handleProjectClose = handleProjectClose; exports.handleProjectOpen = handleProjectOpen; exports._readyPromise = _readyPromise; + exports.filterText = filterText; + exports.postMessage = postMessage; + exports.addPendingRequest = addPendingRequest; }); diff --git a/src/JSUtils/node/TernNodeDomain.js b/src/JSUtils/node/TernNodeDomain.js index 42f3118f1e8..a1d47f0e64e 100644 --- a/src/JSUtils/node/TernNodeDomain.js +++ b/src/JSUtils/node/TernNodeDomain.js @@ -154,7 +154,7 @@ function initTernServer(env, files) { defs: env, async: true, getFile: getFile, - plugins: {requirejs: {}, doc_comment: true, angular: true} + plugins: {requirejs: {}, commonjs: true, doc_comment: true, angular: true} }; // If a server is already created just reset the analysis data before marking it for GC @@ -236,6 +236,44 @@ function buildRequest(fileInfo, query, offset) { return request; } + +/** + * Get all References location + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {{line: number, ch: number}} offset - the offset into the + * file for cursor + */ + function getRefs(fileInfo, offset) { + var request = buildRequest(fileInfo, "refs", offset); + try { + ternServer.request(request, function (error, data) { + if (error) { + _log("Error returned from Tern 'refs' request: " + error); + var response = { + type: MessageIds.TERN_REFS, + error: error.message + }; + self.postMessage(response); + return; + } + var response = { + type: MessageIds.TERN_REFS, + file: fileInfo.name, + offset: offset, + references: data + }; + // Post a message back to the main thread with the results + self.postMessage(response); + }); + } catch (e) { + _reportError(e, fileInfo.name); + } +} + /** * Get definition location * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo @@ -745,6 +783,9 @@ function _requestTernServer(commandConfig) { } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { offset = request.offset; getJumptoDef(request.fileInfo, offset); + } else if (type === MessageIds.TERN_REFS) { + offset = request.offset; + getRefs(request.fileInfo, offset); } else if (type === MessageIds.TERN_ADD_FILES_MSG) { handleAddFiles(request.files); } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { diff --git a/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js b/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js new file mode 100644 index 00000000000..0f06bee0940 --- /dev/null +++ b/src/extensions/default/JavaScriptRefactoring/RefactoringUtils.js @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2013 - present Adobe Systems Incorporated. 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 _ = brackets.getModule("thirdparty/lodash"); + + var AcornLoose = brackets.getModule("thirdparty/acorn/dist/acorn_loose"), + ASTWalker = brackets.getModule("thirdparty/acorn/dist/walk"); + + var templates = JSON.parse(require("text!Templates.json")); + + /** + * Note - To use these state defined in Refactoring Session, + * Please reinitialize this RefactoringSession after performing any of the below operations + * (i.e. replaceRange, setSelection or indentLine) + * + * RefactoringSession objects encapsulate state associated with a refactoring session + * and This will help finding information around documents, selection, + * position, ast, and queries around AST nodes + * + * @constructor + * @param {Editor} editor - the editor context for the session + */ + function RefactoringSession(editor) { + this.editor = editor; + this.document = editor.document; + this.selection = editor.getSelection(); + this.text = this.document.getText(); + this.selectedText = editor.getSelectedText(); + this.cm = editor._codeMirror; + this.startIndex = editor.indexFromPos(this.selection.start); + this.endIndex = editor.indexFromPos(this.selection.end); + this.startPos = this.selection.start; + this.endPos = this.selection.end; + this.ast = this.createAstOfCurrentDoc(); + } + + /** + * Get the end position of given line + * + * @param {number} line - line number + * @return {{line: number, ch: number}} - line end position + */ + RefactoringSession.prototype.lineEndPosition = function (line) { + var lineText = this.document.getLine(line); + + return { + line: line, + ch: lineText.length + }; + }; + + /** + * Get the ast of current opened document in focused editor + * + * @return {Object} - Ast of current opened doc + */ + RefactoringSession.prototype.createAstOfCurrentDoc = function () { + return AcornLoose.parse_dammit(this.document.getText()); + }; + + /** + * This will add template at given position/selection + * + * @param {string} template - name of the template defined in templates.json + * @param {Array} args- Check all arguments that exist in defined templated pass all that args as array + * @param {{line: number, ch: number}} rangeToReplace - Range which we want to replace + * @param {string} subTemplate - If template written under some category + */ + RefactoringSession.prototype.replaceTextFromTemplate = function (template, args, rangeToReplace, subTemplate) { + var templateText = templates[template]; + + if (subTemplate) { + templateText = templateText[subTemplate]; + } + + var compiled = _.template(templateText), + formattedText = compiled(args); + + if (!rangeToReplace) { + rangeToReplace = this.editor.getSelection(); + } + + this.document.replaceRange(formattedText, rangeToReplace.start, rangeToReplace.end); + + var startLine = rangeToReplace.start.line, + endLine = startLine + formattedText.split("\n").length; + + for (var i = startLine + 1; i < endLine; i++) { + this.cm.indentLine(i); + } + }; + + + /* + * Finds the surrounding ast node of the given expression of any of the given types + * @param {!ASTNode} ast + * @param {!{start: number, end: number}} expn - contains start and end offsets of expn + * @param {!Array.} types + * @return {?ASTNode} + */ + RefactoringSession.prototype.findSurroundASTNode = function (ast, expn, types) { + var foundNode = ASTWalker.findNodeAround(ast, expn.start, function (nodeType, node) { + if (expn.end) { + return types.includes(nodeType) && node.end >= expn.end; + } + return types.includes(nodeType); + }); + return foundNode && foundNode.node; + }; + + /* + * Checks whether the text between start and end offsets form a valid set of statements + * @param {!ASTNode} ast - the ast of the complete file + * @param {!number} start - the start offset + * @param {!number} end - the end offset + * @param {!string} fileText - selected text + * @return {boolean} + */ + RefactoringSession.prototype.checkStatement = function (ast, start, end, selectedText, id) { + // Do not allow function or class nodes + var notStatement = false; + ASTWalker.simple(AcornLoose.parse_dammit(selectedText), { + Function: function (node) { + if (node.type === "FunctionDeclaration") { + notStatement = true; + } + }, + Class: function (node) { + notStatement = true; + } + }); + + if (notStatement) { + return false; + } + + var startStatement = this.findSurroundASTNode(ast, {start: start}, ["Statement"]); + var endStatement = this.findSurroundASTNode(ast, {start: end}, ["Statement"]); + + return startStatement && endStatement && startStatement.start === start && + startStatement.end <= end && endStatement.start >= start && + endStatement.end === end; + }; + + /** + * Get Params of selected function + * + * @param {number} start- start offset + * @param {number} end - end offset + * @param {string} selectedText - Create ast for only selected node + * @return {Array} param - Array of all parameters in function + */ + RefactoringSession.prototype.getParamsOfFunction = function getParamsOfFunction(start, end, selectedText) { + var param = []; + ASTWalker.simple(AcornLoose.parse_dammit(selectedText), { + Function: function (node) { + if (node.type === "FunctionDeclaration") { + node.params.forEach(function (item) { + param.push(item.name); + }); + } + } + }); + + return param; + }; + + /** + * Get the Parent node + * + * @param {Object} ast - ast of full document + * @param {number} start - start Offset + * @return {Object} node - Returns the parent node of node which is at offset start + */ + RefactoringSession.prototype.getParentNode = function (ast, start) { + var foundNode = ASTWalker.findNodeAround(ast, start, function(nodeType, node) { + return (nodeType === "ObjectExpression"); + }); + return foundNode && foundNode.node; + }; + + /** + * Checks weather the node at start is last in that scope or not + * + * @param {Object} ast - ast of full document + * @param {number} start - start Offset + * @return {boolean} - is last node in that scope + */ + RefactoringSession.prototype.isLastNodeInScope = function (ast, start) { + var parentNode = this.getParentNode(ast, start), + currentNodeStart; + + ASTWalker.simple(parentNode, { + Property: function (node) { + currentNodeStart = node.start; + } + }); + + return start >= currentNodeStart; + }; + + /** + * Normalize text by removing leading and trailing whitespace characters + * and moves the start and end offset to reflect the new offset + * @param {!string} text - selected text + * @param {!number} start - the start offset of the text + * @param {!number} end - the end offset of the text + * @return {!{text: string, start: number, end: number}} + */ + RefactoringSession.prototype.normalizeText = function normalizeText(text, start, end) { + var trimmedText; + + // Remove leading spaces + trimmedText = _.trimLeft(text); + + if (trimmedText.length < text.length) { + start += (text.length - trimmedText.length); + } + + text = trimmedText; + + // Remove trailing spaces + trimmedText = _.trimRight(text); + + if (trimmedText.length < text.length) { + end -= (text.length - trimmedText.length); + } + + text = trimmedText; + + + return { + text: trimmedText, + start: start, + end: end + }; + }; + + module.exports = RefactoringSession; +}); diff --git a/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js b/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js new file mode 100644 index 00000000000..c587d142124 --- /dev/null +++ b/src/extensions/default/JavaScriptRefactoring/RenameIdentifier.js @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2013 - present Adobe Systems Incorporated. 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 EditorManager = brackets.getModule("editor/EditorManager"), + ScopeManager = brackets.getModule("JSUtils/ScopeManager"), + Session = brackets.getModule("JSUtils/Session"), + MessageIds = brackets.getModule("JSUtils/MessageIds"), + CommandManager = brackets.getModule("command/CommandManager"), + Menus = brackets.getModule("command/Menus"), + Strings = brackets.getModule("strings"); + //Commands + var refactorRename = "javascript.renamereference"; + + var session = null; // object that encapsulates the current session state + + //Create new session + function initializeSession(editor) { + session = new Session(editor); + } + + //Post message to tern node domain that will request tern server to find refs + function getRefs(fileInfo, offset) { + ScopeManager.postMessage({ + type: MessageIds.TERN_REFS, + fileInfo: fileInfo, + offset: offset + }); + + return ScopeManager.addPendingRequest(fileInfo.name, offset, MessageIds.TERN_REFS); + } + + //Create info required to find reference + function requestFindRefs(session, document, offset) { + if (!document || !session) { + return; + } + var path = document.file.fullPath, + fileInfo = { + type: MessageIds.TERN_FILE_INFO_TYPE_FULL, + name: path, + offsetLines: 0, + text: ScopeManager.filterText(session.getJavascriptText()) + }; + var ternPromise = getRefs(fileInfo, offset); + + return {promise: ternPromise}; + } + + //Do rename of identifier which is at cursor + function handleRename() { + var editor = EditorManager.getActiveEditor(), + offset, handleFindRefs; + + if (!editor) { + return; + } + + if (editor.getSelections().length > 1) { + editor.displayErrorMessageAtCursor(Strings.ERROR_RENAME_MULTICURSOR); + return; + } + initializeSession(editor); + + + if (!editor || editor.getModeForSelection() !== "javascript") { + return; + } + + var result = new $.Deferred(); + + function isInSameFile(obj, refsResp) { + return (obj && obj.file === refsResp.file); + } + + /** + * Check if references are in this file only + * If yes then select all references + */ + function handleFindRefs (refsResp) { + if (refsResp && refsResp.references && refsResp.references.refs) { + if (refsResp.references.type === "local") { + EditorManager.getActiveEditor().setSelections(refsResp.references.refs); + } else { + EditorManager.getActiveEditor().setSelections(refsResp.references.refs.filter(function(element) { + return isInSameFile(element, refsResp); + })); + } + } + } + + /** + * Make a find ref request. + * @param {Session} session - the session + * @param {number} offset - the offset of where to jump from + */ + function requestFindReferences(session, offset) { + var response = requestFindRefs(session, session.editor.document, offset); + + if (response && response.hasOwnProperty("promise")) { + response.promise.done(handleFindRefs).fail(function () { + result.reject(); + }); + } + } + + offset = session.getOffset(); + requestFindReferences(session, offset); + + return result.promise(); + } + + //Register command, add menus and context menu, key binding- Ctrl+R + function addCommands() { + + CommandManager.register(Strings.CMD_REFACTORING_RENAME, refactorRename, handleRename); + + var keysRename = [ + {key: "Ctrl-R", platform: "mac"}, // don't translate to Cmd-R on mac + {key: "Ctrl-R", platform: "win"}, + {key: "Ctrl-R", platform: "linux"} + ], + menuLocation = Menus.AppMenuBar.EDIT_MENU, + editorCmenu = Menus.getContextMenu(Menus.ContextMenuIds.EDITOR_MENU); + + if (editorCmenu) { + editorCmenu.addMenuItem(refactorRename); + } + + Menus.getMenu(menuLocation).addMenuDivider(); + Menus.getMenu(menuLocation).addMenuItem(refactorRename, keysRename); + } + + exports.addCommands = addCommands; +}); diff --git a/src/extensions/default/JavaScriptRefactoring/Templates.json b/src/extensions/default/JavaScriptRefactoring/Templates.json new file mode 100644 index 00000000000..925399c273c --- /dev/null +++ b/src/extensions/default/JavaScriptRefactoring/Templates.json @@ -0,0 +1,14 @@ +{ + "wrapCondition": "if (Condition) {\n${body}\n}", + + "arrowFunction": { + "oneParamOneStament": "${params} => ${statement}", + "oneParamManyStament": "${params} => ", + "manyParamOneStament": "(${params}) => ${statement}", + "manyParamManyStament": "(${params}) => " + }, + + "tryCatch": "try {\n${body}\n} catch (e) {\n\/\/Catch Statement\n}", + + "gettersSetters": "\nget ${getName}() {\nreturn this.${tokenName};\n},\n\nset ${setName}(val) {\nthis.${tokenName} = val;\n}" +} \ No newline at end of file diff --git a/src/extensions/default/JavaScriptRefactoring/WrapSelection.js b/src/extensions/default/JavaScriptRefactoring/WrapSelection.js new file mode 100644 index 00000000000..fa84817c221 --- /dev/null +++ b/src/extensions/default/JavaScriptRefactoring/WrapSelection.js @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2013 - present Adobe Systems Incorporated. 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 _ = brackets.getModule("thirdparty/lodash"); + + var EditorManager = brackets.getModule("editor/EditorManager"), + TokenUtils = brackets.getModule("utils/TokenUtils"), + CommandManager = brackets.getModule("command/CommandManager"), + Menus = brackets.getModule("command/Menus"), + Strings = brackets.getModule("strings"), + RefactoringSession = require("RefactoringUtils"); + + //Template keys mentioned in Templates.json + var WRAP_IN_CONDITION = "wrapCondition", + ARROW_FUNCTION = "arrowFunction", + GETTERS_SETTERS = "gettersSetters", + TRY_CATCH = "tryCatch"; + + //Commands + var refactorWrapInTryCatch = "refactoring.wrapintrycatch", + refactorWrapInCondition = "refactoring.wrapincondition", + refactorConvertToArrowFn = "refactoring.converttoarrowfunction", + refactorCreateGetSet = "refactoring.creategettersandsetters"; + + //Active session which will contain information about editor, selection etc + var current = null; + + /** + * Initialize session + */ + function initializeRefactoringSession(editor) { + current = new RefactoringSession(editor); + } + + /** + * Wrap selected statements + * + * @param {string} wrapperName - template name where we want wrap selected statements + * @param {string} err- error message if we can't wrap selected code + */ + function _wrapSelectedStatements (wrapperName, err) { + var editor = EditorManager.getActiveEditor(); + if (!editor) { + return; + } + initializeRefactoringSession(editor); + + var startIndex = current.startIndex, + endIndex = current.endIndex, + selectedText = current.selectedText, + pos; + + if (selectedText.length === 0) { + var statementNode = current.findSurroundASTNode(current.ast, {start: startIndex}, ["Statement"]); + selectedText = current.text.substr(statementNode.start, statementNode.end - statementNode.start); + startIndex = statementNode.start; + endIndex = statementNode.end; + } else { + var selectionDetails = current.normalizeText(selectedText, startIndex, endIndex); + selectedText = selectionDetails.text; + startIndex = selectionDetails.start; + endIndex = selectionDetails.end; + } + + if (!current.checkStatement(current.ast, startIndex, endIndex, selectedText)) { + current.editor.displayErrorMessageAtCursor(err); + return; + } + + pos = { + "start": current.cm.posFromIndex(startIndex), + "end": current.cm.posFromIndex(endIndex) + }; + + current.document.batchOperation(function() { + current.replaceTextFromTemplate(wrapperName, {body: selectedText}, pos); + }); + + if (wrapperName === TRY_CATCH) { + var cursorLine = current.editor.getSelection().start.line - 1, + startCursorCh = current.document.getLine(cursorLine).indexOf("\/\/"), + endCursorCh = current.document.getLine(cursorLine).length; + + current.editor.setSelection({"line": cursorLine, "ch": startCursorCh}, {"line": cursorLine, "ch": endCursorCh}); + } else if (wrapperName === WRAP_IN_CONDITION) { + current.editor.setSelection({"line": pos.start.line, "ch": pos.start.ch + 4}, {"line": pos.start.line, "ch": pos.start.ch + 13}); + } + } + + + //Wrap selected statements in try catch block + function wrapInTryCatch() { + _wrapSelectedStatements(TRY_CATCH, Strings.ERROR_TRY_CATCH); + } + + //Wrap selected statements in try condition + function wrapInCondition() { + _wrapSelectedStatements(WRAP_IN_CONDITION, Strings.ERROR_WRAP_IN_CONDITION); + } + + //Convert function to arrow function + function convertToArrowFunction() { + var editor = EditorManager.getActiveEditor(); + if (!editor) { + return; + } + initializeRefactoringSession(editor); + //Handle when there is no selected line + var funcExprNode = current.findSurroundASTNode(current.ast, {start: current.startIndex}, ["FunctionExpression"]); + + if (!funcExprNode || funcExprNode.type !== "FunctionExpression" || funcExprNode.id) { + current.editor.displayErrorMessageAtCursor(Strings.ERROR_ARROW_FUNCTION); + return; + } + var noOfStatements = funcExprNode.body.body.length, + selectedText = current.text.substr(funcExprNode.start, funcExprNode.end - funcExprNode.start), + param = []; + + funcExprNode.params.forEach(function (item) { + param.push(item.name); + }); + + var loc = { + "fullFunctionScope": { + start: funcExprNode.start, + end: funcExprNode.end + }, + "functionsDeclOnly": { + start: funcExprNode.start, + end: funcExprNode.body.start + } + }, + locPos = { + "fullFunctionScope": { + "start": current.cm.posFromIndex(loc.fullFunctionScope.start), + "end": current.cm.posFromIndex(loc.fullFunctionScope.end) + }, + "functionsDeclOnly": { + "start": current.cm.posFromIndex(loc.functionsDeclOnly.start), + "end": current.cm.posFromIndex(loc.functionsDeclOnly.end) + } + }, + isReturnStatement = funcExprNode.body.body[0].type === "ReturnStatement", + bodyStatements = funcExprNode.body.body[0], + params = { + "params": param.join(", "), + "statement": _.trimRight(current.text.substr(bodyStatements.start, bodyStatements.end - bodyStatements.start), ";") + }; + + if (isReturnStatement) { + params.statement = params.statement.substr(7).trim(); + } + + if (noOfStatements === 1) { + current.document.batchOperation(function() { + funcExprNode.params.length === 1 ? current.replaceTextFromTemplate(ARROW_FUNCTION, params, locPos.fullFunctionScope, "oneParamOneStament") : + current.replaceTextFromTemplate(ARROW_FUNCTION, params, locPos.fullFunctionScope, "manyParamOneStament"); + + }); + } else { + current.document.batchOperation(function() { + funcExprNode.params.length === 1 ? current.replaceTextFromTemplate(ARROW_FUNCTION, {params: param}, + locPos.functionsDeclOnly, "oneParamManyStament") : + current.replaceTextFromTemplate(ARROW_FUNCTION, {params: param.join(", ")}, locPos.functionsDeclOnly, "manyParamManyStament"); + }); + } + + current.editor.setCursorPos(locPos.functionsDeclOnly.end.line, locPos.functionsDeclOnly.end.ch, false); + } + + // Create gtteres and setters for a property + function createGettersAndSetters() { + var editor = EditorManager.getActiveEditor(); + if (!editor) { + return; + } + initializeRefactoringSession(editor); + + var startIndex = current.startIndex, + endIndex = current.endIndex, + selectedText = current.selectedText; + + if (selectedText.length >= 1) { + var selectionDetails = current.normalizeText(selectedText, startIndex, endIndex); + selectedText = selectionDetails.text; + startIndex = selectionDetails.start; + endIndex = selectionDetails.end; + } + + var token = TokenUtils.getTokenAt(current.cm, current.cm.posFromIndex(endIndex)), + isLastNode, + lineEndPos, + templateParams; + + //Create getters and setters only if selected reference is a property + if (token.type !== "property") { + current.editor.displayErrorMessageAtCursor(Strings.ERROR_GETTERS_SETTERS); + return; + } + + // Check if selected propery is child of a object expression + if (!current.getParentNode(current.ast, endIndex)) { + current.editor.displayErrorMessageAtCursor(Strings.ERROR_GETTERS_SETTERS); + return; + } + + //We have to add ',' so we need to find position of current property selected + isLastNode = current.isLastNodeInScope(current.ast, endIndex); + lineEndPos = current.lineEndPosition(current.startPos.line); + templateParams = { + "getName": token.string, + "setName": token.string, + "tokenName": token.string + }; + + // Replace, setSelection, IndentLine + // We need to call batchOperation as indentLine don't have option to add origin as like replaceRange + current.document.batchOperation(function() { + if (isLastNode) { + //Add ',' in the end of current line + current.document.replaceRange(",", lineEndPos, lineEndPos); + lineEndPos.ch++; + } + + current.editor.setSelection(lineEndPos); //Selection on line end + + // Add getters and setters for given token using template at current cursor position + current.replaceTextFromTemplate(GETTERS_SETTERS, templateParams); + + if (!isLastNode) { + // Add ',' at the end setter + current.document.replaceRange(",", current.editor.getSelection().start, current.editor.getSelection().start); + } + }); + } + + + //Register commands and and menus in conext menu and main menus under 'Edit' + function addCommands() { + CommandManager.register(Strings.CMD_REFACTORING_TRY_CATCH, refactorWrapInTryCatch, wrapInTryCatch); + CommandManager.register(Strings.CMD_REFACTORING_CONDITION, refactorWrapInCondition, wrapInCondition); + CommandManager.register(Strings.CMD_REFACTORING_ARROW_FUNCTION, refactorConvertToArrowFn, convertToArrowFunction); + CommandManager.register(Strings.CMD_REFACTORING_GETTERS_SETTERS, refactorCreateGetSet, createGettersAndSetters); + + var menuLocation = Menus.AppMenuBar.EDIT_MENU, + editorCmenu = Menus.getContextMenu(Menus.ContextMenuIds.EDITOR_MENU); + + if (editorCmenu) { + editorCmenu.addMenuItem(refactorWrapInTryCatch); + editorCmenu.addMenuItem(refactorWrapInCondition); + editorCmenu.addMenuItem(refactorConvertToArrowFn); + editorCmenu.addMenuItem(refactorCreateGetSet); + } + + Menus.getMenu(menuLocation).addMenuItem(refactorWrapInTryCatch); + Menus.getMenu(menuLocation).addMenuItem(refactorWrapInCondition); + Menus.getMenu(menuLocation).addMenuItem(refactorConvertToArrowFn); + Menus.getMenu(menuLocation).addMenuItem(refactorCreateGetSet); + } + + exports.addCommands = addCommands; +}); diff --git a/src/extensions/default/JavaScriptRefactoring/main.js b/src/extensions/default/JavaScriptRefactoring/main.js new file mode 100644 index 00000000000..4bc7096ba3a --- /dev/null +++ b/src/extensions/default/JavaScriptRefactoring/main.js @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2013 - present Adobe Systems Incorporated. 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 = brackets.getModule("utils/AppInit"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), + RenameIdentifier = require("RenameIdentifier"), + WrapSelection = require("WrapSelection"); + + var jsRefactoringEnabled = true; + + + // This preference controls whether to create a session and process all JS files or not. + PreferencesManager.definePreference("refactoring.JSRefactoring", "boolean", true, { + description: Strings.DESCRIPTION_CODE_REFACTORING + }); + + + /** + * Check whether any of refactoring hints preferences for JS Refactoring is disabled + * @return {boolean} enabled/disabled + */ + function _isRefactoringEnabled() { + return (PreferencesManager.get("refactoring.JSRefactoring") !== false); + } + + PreferencesManager.on("change", "refactoring.JSRefactoring", function () { + jsRefactoringEnabled = _isRefactoringEnabled(); + }); + + AppInit.appReady(function () { + + + //TODO- Add submenus instead of context menu + if (jsRefactoringEnabled) { + RenameIdentifier.addCommands(); + WrapSelection.addCommands(); + } + }); +}); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 0bc68969f0f..d2f559fac8b 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -674,6 +674,20 @@ define({ "DETECTED_EXCLUSION_TITLE" : "JavaScript File Inference Problem", "DETECTED_EXCLUSION_INFO" : "{APP_NAME} ran into trouble processing {0}.

This file will no longer be processed for code hints, Jump to Definition or Quick Edit. To re-enable this file, open .brackets.json in your project and edit jscodehints.detectedExclusions.

This is likely a {APP_NAME} bug. If you can provide a copy of this file, please file a bug with a link to the file named here.", + + // extensions/default/JavaScriptRefactoring + "CMD_REFACTORING_RENAME" : "Rename", + "CMD_REFACTORING_TRY_CATCH" : "Wrap in Try Catch", + "CMD_REFACTORING_CONDITION" : "Wrap in Condition", + "CMD_REFACTORING_GETTERS_SETTERS" : "Create Getters Setters", + "CMD_REFACTORING_ARROW_FUNCTION" : "Convert to Arrow Function", + "DESCRIPTION_CODE_REFACTORING" : "Enable/disable JavaScript Code Refactoring", + "ERROR_TRY_CATCH" : "Select valid code to wrap in a Try-catch block", + "ERROR_WRAP_IN_CONDITION" : "Select valid code to wrap in a Condition block", + "ERROR_ARROW_FUNCTION" : "Place the cursor inside a function expression", + "ERROR_GETTERS_SETTERS" : "Place the cursor at a member of an object expression", + "ERROR_RENAME_MULTICURSOR" : "Cannot rename when using multi-cursors", + // extensions/default/JSLint "JSLINT_NAME" : "JSLint",