diff --git a/src-node/utils.js b/src-node/utils.js index b648d8fdab..37094812ef 100644 --- a/src-node/utils.js +++ b/src-node/utils.js @@ -3,6 +3,7 @@ const { exec, execFile } = require('child_process'); const fs = require('fs'); const fsPromise = require('fs').promises; const path = require('path'); +const os = require('os'); const {lintFile} = require("./ESLint/service"); let openModule, open; // dynamic import when needed @@ -146,6 +147,86 @@ async function _npmInstallInFolder({moduleNativeDir}) { }); } +/** + * If it's a dir that exists, returns that + * If it's a file, it returns the parent directory if it exists + * If no parent exists, it returns the original path. + * + * @param {string} cwd - The path to validate. + * @returns {string} - An existing directory or the original path. + */ +function _getValidDirectory(cwd) { + let currentPath = path.resolve(cwd); + const exists = fs.existsSync(currentPath); + + if (exists) { + const isPathDir = fs.statSync(currentPath).isDirectory(); + if(isPathDir){ + return currentPath; + } + return path.dirname(currentPath); + } + + currentPath = path.dirname(currentPath); + if(fs.existsSync(currentPath)){ + return currentPath; + } + + // If no valid directory is found, fallback to the original cwd + return cwd; +} + +/** + * Opens a native terminal window with the specified current working directory. + * Returns a Promise that resolves if the terminal starts successfully, or rejects if it fails. + * + * @param {string} cwd - The directory to open the terminal in. + * @param {boolean} usePowerShell - Whether to use PowerShell instead of cmd on Windows. + * @returns {Promise} - Resolves if the terminal starts, rejects otherwise. + */ +function openNativeTerminal({cwd, usePowerShell = false}) { + return new Promise((resolve, reject) => { + const platform = os.platform(); + cwd = _getValidDirectory(cwd); + let command; + + if (platform === 'win32') { + if (usePowerShell) { + command = `start powershell -NoExit -Command "Set-Location -Path '${cwd}'"`; + } else { + command = `start cmd /K "cd /D ${cwd}"`; + } + } else if (platform === 'darwin') { + command = `open -a Terminal "${cwd}"`; + } else { + command = ` + if command -v gnome-terminal > /dev/null 2>&1; then + gnome-terminal --working-directory="${cwd}"; + elif command -v konsole > /dev/null 2>&1; then + konsole --workdir "${cwd}"; + elif command -v xfce4-terminal > /dev/null 2>&1; then + xfce4-terminal --working-directory="${cwd}"; + elif command -v xterm > /dev/null 2>&1; then + xterm -e "cd '${cwd}' && bash"; + else + echo "No supported terminal emulator found."; + exit 1; + fi + `; + } + + // Execute the terminal command + exec(command, (error) => { + if (error) { + reject(new Error(`Failed to start terminal: ${error.message}`)); + } else { + resolve(); + } + }); + }); +} + + async function ESLintFile({text, fullFilePath, projectFullPath}) { return lintFile(text, fullFilePath, projectFullPath); } @@ -161,5 +242,6 @@ exports.getLinuxOSFlavorName = getLinuxOSFlavorName; exports.openUrlInBrowser = openUrlInBrowser; exports.getEnvironmentVariable = getEnvironmentVariable; exports.ESLintFile = ESLintFile; +exports.openNativeTerminal = openNativeTerminal; exports._loadNodeExtensionModule = _loadNodeExtensionModule; exports._npmInstallInFolder = _npmInstallInFolder; diff --git a/src/command/Commands.js b/src/command/Commands.js index b4eb6077be..e95e0e5b6d 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -267,6 +267,12 @@ define(function (require, exports, module) { /** Submenu for zoom options */ exports.VIEW_ZOOM_SUBMENU = "zoom-view-submenu"; + /** Submenu for Open in project context menu */ + exports.OPEN_IN_SUBMENU = "file-open-in-submenu"; + + /** Submenu for Open in working set context menu */ + exports.OPEN_IN_SUBMENU_WS = "file-open-in-submenu-ws"; + /** Increases editor font size */ exports.VIEW_INCREASE_FONT_SIZE = "view.increaseFontSize"; // ViewCommandHandlers.js _handleIncreaseFontSize() @@ -334,6 +340,12 @@ define(function (require, exports, module) { /** Shows current file in OS file explorer */ exports.NAVIGATE_SHOW_IN_OS = "navigate.showInOS"; // DocumentCommandHandlers.js handleShowInOS() + /** Shows current file in OS Terminal */ + exports.NAVIGATE_OPEN_IN_TERMINAL = "navigate.openInTerminal"; + + /** Shows current file in open powershell in Windows os */ + exports.NAVIGATE_OPEN_IN_POWERSHELL = "navigate.openInPowerShell"; + /** Opens quick open dialog */ exports.NAVIGATE_QUICK_OPEN = "navigate.quickOpen"; // QuickOpen.js doFileSearch() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 47f74c3cfa..d01b0656ac 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -58,7 +58,7 @@ define(function (require, exports, module) { return err; } _setContextMenuItemsVisible(isPresent, [Commands.FILE_RENAME, - Commands.NAVIGATE_SHOW_IN_FILE_TREE, Commands.NAVIGATE_SHOW_IN_OS]); + Commands.NAVIGATE_SHOW_IN_FILE_TREE, Commands.NAVIGATE_SHOW_IN_OS, Commands.NAVIGATE_OPEN_IN_TERMINAL]); }); } } @@ -297,7 +297,12 @@ define(function (require, exports, module) { workingset_cmenu.addMenuItem(Commands.FILE_SAVE); workingset_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); if(Phoenix.isNativeApp){ - workingset_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS); + let subMenu = workingset_cmenu.addSubMenu(Strings.CMD_OPEN_IN, Commands.OPEN_IN_SUBMENU_WS); + subMenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS); + subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_TERMINAL); + if (brackets.platform === "win") { + subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_POWERSHELL); + } } workingset_cmenu.addMenuDivider(); workingset_cmenu.addMenuItem(Commands.FILE_COPY); @@ -331,7 +336,12 @@ define(function (require, exports, module) { project_cmenu.addMenuItem(Commands.FILE_NEW); project_cmenu.addMenuItem(Commands.FILE_NEW_FOLDER); if(Phoenix.isNativeApp){ - project_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS); + let subMenu = project_cmenu.addSubMenu(Strings.CMD_OPEN_IN, Commands.OPEN_IN_SUBMENU); + subMenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS); + subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_TERMINAL); + if (brackets.platform === "win") { + subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_POWERSHELL); + } } project_cmenu.addMenuDivider(); project_cmenu.addMenuItem(Commands.FILE_CUT); diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 55f3801438..9fdd67420a 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -58,6 +58,7 @@ define(function (require, exports, module) { LanguageManager = require("language/LanguageManager"), NewFileContentManager = require("features/NewFileContentManager"), NodeConnector = require("NodeConnector"), + NodeUtils = require("utils/NodeUtils"), _ = require("thirdparty/lodash"); /** @@ -1959,6 +1960,20 @@ define(function (require, exports, module) { } } + function openDefaultTerminal() { + const entry = ProjectManager.getSelectedItem(); + if (entry && entry.fullPath) { + NodeUtils.openNativeTerminal(entry.fullPath); + } + } + + function openPowerShell() { + const entry = ProjectManager.getSelectedItem(); + if (entry && entry.fullPath) { + NodeUtils.openNativeTerminal(entry.fullPath, true); + } + } + function raceAgainstTime(promise, timeout = 2000) { const timeoutPromise = new Promise((_resolve, reject) => { setTimeout(() => { @@ -2251,11 +2266,13 @@ define(function (require, exports, module) { exports._parseDecoratedPath = _parseDecoratedPath; // Set some command strings - var quitString = Strings.CMD_QUIT, - showInOS = Strings.CMD_SHOW_IN_OS; + let quitString = Strings.CMD_QUIT, + showInOS = Strings.CMD_SHOW_IN_FILE_MANAGER, + defaultTerminal = Strings.CMD_OPEN_IN_TERMINAL; if (brackets.platform === "win") { quitString = Strings.CMD_EXIT; showInOS = Strings.CMD_SHOW_IN_EXPLORER; + defaultTerminal = Strings.CMD_OPEN_IN_CMD; } else if (brackets.platform === "mac") { showInOS = Strings.CMD_SHOW_IN_FINDER; } @@ -2304,6 +2321,10 @@ define(function (require, exports, module) { // Special Commands CommandManager.register(showInOS, Commands.NAVIGATE_SHOW_IN_OS, handleShowInOS); + CommandManager.register(defaultTerminal, Commands.NAVIGATE_OPEN_IN_TERMINAL, openDefaultTerminal); + if (brackets.platform === "win") { + CommandManager.register(Strings.CMD_OPEN_IN_POWER_SHELL, Commands.NAVIGATE_OPEN_IN_POWERSHELL, openPowerShell); + } CommandManager.register(Strings.CMD_NEW_BRACKETS_WINDOW, Commands.FILE_NEW_WINDOW, handleFileNewWindow); CommandManager.register(quitString, Commands.FILE_QUIT, handleFileCloseWindow); CommandManager.register(Strings.CMD_SHOW_IN_TREE, Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInTree); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index c9f8554f6b..af58596188 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -495,6 +495,7 @@ define({ "CMD_FILE_REFRESH": "Refresh File Tree", "CMD_FILE_SHOW_FOLDERS_FIRST": "Sort Folders First", "CMD_QUIT": "Quit", + "CMD_OPEN_IN": "Open In", // Used in native File menu on Windows "CMD_EXIT": "Exit", @@ -594,9 +595,12 @@ define({ "CMD_NEXT_DOC_LIST_ORDER": "Next Document in List", "CMD_PREV_DOC_LIST_ORDER": "Previous Document in List", "CMD_SHOW_IN_TREE": "Show in File Tree", - "CMD_SHOW_IN_EXPLORER": "Show in Explorer", - "CMD_SHOW_IN_FINDER": "Show in Finder", - "CMD_SHOW_IN_OS": "Show in OS Files", + "CMD_SHOW_IN_EXPLORER": "Windows File Explorer", + "CMD_SHOW_IN_FINDER": "macOS Finder", + "CMD_SHOW_IN_FILE_MANAGER": "File Manager", + "CMD_OPEN_IN_TERMINAL": "Terminal", + "CMD_OPEN_IN_CMD": "Command Prompt", + "CMD_OPEN_IN_POWER_SHELL": "Power Shell", "CMD_SWITCH_PANE_FOCUS": "Switch Pane Focus", // Debug menu commands diff --git a/src/utils/NodeUtils.js b/src/utils/NodeUtils.js index ce838e65ec..fa1d79b958 100644 --- a/src/utils/NodeUtils.js +++ b/src/utils/NodeUtils.js @@ -159,6 +159,23 @@ define(function (require, exports, module) { }); } + /** + * Runs ESLint on a file + * This is only available in the native app + * + * @param {string} cwd the working directory of terminal + * @param {boolean} [usePowerShell] + */ + async function openNativeTerminal(cwd, usePowerShell = false) { + if(!Phoenix.isNativeApp) { + throw new Error("openNativeTerminal not available in browser"); + } + return utilsConnector.execPeer("openNativeTerminal", { + cwd: window.fs.getTauriPlatformPath(cwd), + usePowerShell + }); + } + if(NodeConnector.isNodeAvailable()) { // todo we need to update the strings if a user extension adds its translations. Since we dont support // node extensions for now, should consider when we support node extensions. @@ -195,6 +212,7 @@ define(function (require, exports, module) { exports.openUrlInBrowser = openUrlInBrowser; exports.ESLintFile = ESLintFile; exports.getEnvironmentVariable = getEnvironmentVariable; + exports.openNativeTerminal = openNativeTerminal; /** * checks if Node connector is ready