From b2fcba25c8dd04318420af30f877b0984a524055 Mon Sep 17 00:00:00 2001
From: Vadim ProductEngine <vsavchuk@productengine.com>
Date: Sat, 13 Nov 2010 00:53:29 +0200
Subject: [PATCH] STORM-52 FIXED Made it possible to use an external script
 editor.

The editor can be specified:
* via "ExternalEditor" setting in settings.xml
* via LL_SCRIPT_EDITOR variable

Removed obsolete XUIEditor setting in favor of the new one.
---
 indra/llcommon/llprocesslauncher.cpp          |   5 +
 indra/llcommon/llprocesslauncher.h            |   2 +
 indra/newview/CMakeLists.txt                  |   2 +
 indra/newview/app_settings/settings.xml       |   4 +-
 indra/newview/llexternaleditor.cpp            | 192 ++++++++++++++
 indra/newview/llexternaleditor.h              |  91 +++++++
 indra/newview/llfloateruipreview.cpp          | 210 +++-------------
 indra/newview/llpreviewscript.cpp             | 235 ++++++++++++++----
 indra/newview/llpreviewscript.h               |  15 +-
 .../skins/default/xui/en/panel_script_ed.xml  |   9 +
 10 files changed, 537 insertions(+), 228 deletions(-)
 create mode 100644 indra/newview/llexternaleditor.cpp
 create mode 100644 indra/newview/llexternaleditor.h

diff --git a/indra/llcommon/llprocesslauncher.cpp b/indra/llcommon/llprocesslauncher.cpp
index 99308c94e7c..81e5f8820d7 100644
--- a/indra/llcommon/llprocesslauncher.cpp
+++ b/indra/llcommon/llprocesslauncher.cpp
@@ -58,6 +58,11 @@ void LLProcessLauncher::setWorkingDirectory(const std::string &dir)
 	mWorkingDir = dir;
 }
 
+const std::string& LLProcessLauncher::getExecutable() const
+{
+	return mExecutable;
+}
+
 void LLProcessLauncher::clearArguments()
 {
 	mLaunchArguments.clear();
diff --git a/indra/llcommon/llprocesslauncher.h b/indra/llcommon/llprocesslauncher.h
index 479aeb664a5..954c2491472 100644
--- a/indra/llcommon/llprocesslauncher.h
+++ b/indra/llcommon/llprocesslauncher.h
@@ -47,6 +47,8 @@ class LL_COMMON_API LLProcessLauncher
 	void setExecutable(const std::string &executable);
 	void setWorkingDirectory(const std::string &dir);
 
+	const std::string& getExecutable() const;
+
 	void clearArguments();
 	void addArgument(const std::string &arg);
 	void addArgument(const char *arg);
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 09622d3af58..d44b0ce6799 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -143,6 +143,7 @@ set(viewer_SOURCE_FILES
     lleventnotifier.cpp
     lleventpoll.cpp
     llexpandabletextbox.cpp
+    llexternaleditor.cpp
     llface.cpp
     llfasttimerview.cpp
     llfavoritesbar.cpp
@@ -674,6 +675,7 @@ set(viewer_HEADER_FILES
     lleventnotifier.h
     lleventpoll.h
     llexpandabletextbox.h
+    llexternaleditor.h
     llface.h
     llfasttimerview.h
     llfavoritesbar.h
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index ebd93b59875..a5d9bd0f4f1 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -11883,10 +11883,10 @@
       <key>Value</key>
       <real>150000.0</real>
     </map>
-    <key>XUIEditor</key>
+    <key>ExternalEditor</key>
     <map>
       <key>Comment</key>
-      <string>Path to program used to edit XUI files</string>
+      <string>Path to program used to edit LSL scripts and XUI files, e.g.: /usr/bin/gedit --new-window "%s"</string>
       <key>Persist</key>
       <integer>1</integer>
       <key>Type</key>
diff --git a/indra/newview/llexternaleditor.cpp b/indra/newview/llexternaleditor.cpp
new file mode 100644
index 00000000000..54968841ab1
--- /dev/null
+++ b/indra/newview/llexternaleditor.cpp
@@ -0,0 +1,192 @@
+/** 
+ * @file llexternaleditor.cpp
+ * @brief A convenient class to run external editor.
+ *
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+#include "llexternaleditor.h"
+
+#include "llui.h"
+
+// static
+const std::string LLExternalEditor::sFilenameMarker = "%s";
+
+// static
+const std::string LLExternalEditor::sSetting = "ExternalEditor";
+
+bool LLExternalEditor::setCommand(const std::string& env_var, const std::string& override)
+{
+	std::string cmd = findCommand(env_var, override);
+	if (cmd.empty())
+	{
+		llwarns << "Empty editor command" << llendl;
+		return false;
+	}
+
+	// Add the filename marker if missing.
+	if (cmd.find(sFilenameMarker) == std::string::npos)
+	{
+		cmd += " \"" + sFilenameMarker + "\"";
+		llinfos << "Adding the filename marker (" << sFilenameMarker << ")" << llendl;
+	}
+
+	string_vec_t tokens;
+	if (tokenize(tokens, cmd) < 2) // 2 = bin + at least one arg (%s)
+	{
+		llwarns << "Error parsing editor command" << llendl;
+		return false;
+	}
+
+	// Check executable for existence.
+	std::string bin_path = tokens[0];
+	if (!LLFile::isfile(bin_path))
+	{
+		llwarns << "Editor binary [" << bin_path << "] not found" << llendl;
+		return false;
+	}
+
+	// Save command.
+	mProcess.setExecutable(bin_path);
+	mArgs.clear();
+	for (size_t i = 1; i < tokens.size(); ++i)
+	{
+		if (i > 1) mArgs += " ";
+		mArgs += "\"" + tokens[i] + "\"";
+	}
+	llinfos << "Setting command [" << bin_path << " " << mArgs << "]" << llendl;
+
+	return true;
+}
+
+bool LLExternalEditor::run(const std::string& file_path)
+{
+	std::string args = mArgs;
+	if (mProcess.getExecutable().empty() || args.empty())
+	{
+		llwarns << "Editor command not set" << llendl;
+		return false;
+	}
+
+	// Substitute the filename marker in the command with the actual passed file name.
+	LLStringUtil::replaceString(args, sFilenameMarker, file_path);
+
+	// Split command into separate tokens.
+	string_vec_t tokens;
+	tokenize(tokens, args);
+
+	// Set process arguments taken from the command.
+	mProcess.clearArguments();
+	for (string_vec_t::const_iterator arg_it = tokens.begin(); arg_it != tokens.end(); ++arg_it)
+	{
+		mProcess.addArgument(*arg_it);
+	}
+
+	// Run the editor.
+	llinfos << "Running editor command [" << mProcess.getExecutable() + " " + args << "]" << llendl;
+	int result = mProcess.launch();
+	if (result == 0)
+	{
+		// Prevent killing the process in destructor (will add it to the zombies list).
+		mProcess.orphan();
+	}
+
+	return result == 0;
+}
+
+// static
+size_t LLExternalEditor::tokenize(string_vec_t& tokens, const std::string& str)
+{
+	tokens.clear();
+
+	// Split the argument string into separate strings for each argument
+	typedef boost::tokenizer< boost::char_separator<char> > tokenizer;
+	boost::char_separator<char> sep("", "\" ", boost::drop_empty_tokens);
+
+	tokenizer tokens_list(str, sep);
+	tokenizer::iterator token_iter;
+	BOOL inside_quotes = FALSE;
+	BOOL last_was_space = FALSE;
+	for (token_iter = tokens_list.begin(); token_iter != tokens_list.end(); ++token_iter)
+	{
+		if (!strncmp("\"",(*token_iter).c_str(),2))
+		{
+			inside_quotes = !inside_quotes;
+		}
+		else if (!strncmp(" ",(*token_iter).c_str(),2))
+		{
+			if(inside_quotes)
+			{
+				tokens.back().append(std::string(" "));
+				last_was_space = TRUE;
+			}
+		}
+		else
+		{
+			std::string to_push = *token_iter;
+			if (last_was_space)
+			{
+				tokens.back().append(to_push);
+				last_was_space = FALSE;
+			}
+			else
+			{
+				tokens.push_back(to_push);
+			}
+		}
+	}
+
+	return tokens.size();
+}
+
+// static
+std::string LLExternalEditor::findCommand(
+	const std::string& env_var,
+	const std::string& override)
+{
+	std::string cmd;
+
+	// Get executable path.
+	if (!override.empty())	// try the supplied override first
+	{
+		cmd = override;
+		llinfos << "Using override" << llendl;
+	}
+	else if (!LLUI::sSettingGroups["config"]->getString(sSetting).empty())
+	{
+		cmd = LLUI::sSettingGroups["config"]->getString(sSetting);
+		llinfos << "Using setting" << llendl;
+	}
+	else					// otherwise use the path specified by the environment variable
+	{
+		char* env_var_val = getenv(env_var.c_str());
+		if (env_var_val)
+		{
+			cmd = env_var_val;
+			llinfos << "Using env var " << env_var << llendl;
+		}
+	}
+
+	llinfos << "Found command [" << cmd << "]" << llendl;
+	return cmd;
+}
diff --git a/indra/newview/llexternaleditor.h b/indra/newview/llexternaleditor.h
new file mode 100644
index 00000000000..6ea210d5e22
--- /dev/null
+++ b/indra/newview/llexternaleditor.h
@@ -0,0 +1,91 @@
+/** 
+ * @file llexternaleditor.h
+ * @brief A convenient class to run external editor.
+ *
+ * $LicenseInfo:firstyear=2010&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLEXTERNALEDITOR_H
+#define LL_LLEXTERNALEDITOR_H
+
+#include <llprocesslauncher.h>
+
+/**
+ * Usage:
+ *  LLExternalEditor ed;
+ *  ed.setCommand("MY_EXTERNAL_EDITOR_VAR");
+ *  ed.run("/path/to/file1");
+ *  ed.run("/other/path/to/file2");
+ */
+class LLExternalEditor
+{
+	typedef std::vector<std::string> string_vec_t;
+
+public:
+
+	/**
+	 * Set editor command.
+	 *
+	 * @param env_var			Environment variable of the same purpose.
+	 * @param override			Optional override.
+	 *
+	 * First tries the override, then a predefined setting (sSetting),
+	 * then the environment variable.
+	 *
+	 * @return Command if found, empty string otherwise.
+	 *
+	 * @see sSetting
+	 */
+	bool setCommand(const std::string& env_var, const std::string& override = LLStringUtil::null);
+
+	/**
+	 * Run the editor with the given file.
+	 *
+	 * @param file_path File to edit.
+	 * @return true on success, false on error.
+	 */
+	bool run(const std::string& file_path);
+
+private:
+
+	static std::string findCommand(
+		const std::string& env_var,
+		const std::string& override);
+
+	static size_t tokenize(string_vec_t& tokens, const std::string& str);
+
+	/**
+	 * Filename placeholder that gets replaced with an actual file name.
+	 */
+	static const std::string sFilenameMarker;
+
+	/**
+	 * Setting that can specify the editor command.
+	 */
+	static const std::string sSetting;
+
+
+	std::string			mArgs;
+	LLProcessLauncher	mProcess;
+};
+
+#endif // LL_LLEXTERNALEDITOR_H
diff --git a/indra/newview/llfloateruipreview.cpp b/indra/newview/llfloateruipreview.cpp
index 5dc8067648e..d3a2f144d9f 100644
--- a/indra/newview/llfloateruipreview.cpp
+++ b/indra/newview/llfloateruipreview.cpp
@@ -36,6 +36,7 @@
 
 // Internal utility
 #include "lleventtimer.h"
+#include "llexternaleditor.h"
 #include "llrender.h"
 #include "llsdutil.h"
 #include "llxmltree.h"
@@ -160,6 +161,8 @@ class LLFloaterUIPreview : public LLFloater
 	DiffMap mDiffsMap;							// map, of filename to pair of list of changed element paths and list of errors
 
 private:
+	LLExternalEditor mExternalEditor;
+
 	// XUI elements for this floater
 	LLScrollListCtrl*			mFileList;							// scroll list control for file list
 	LLLineEditor*				mEditorPathTextBox;					// text field for path to editor executable
@@ -185,7 +188,7 @@ class LLFloaterUIPreview : public LLFloater
 	std::string					mSavedDiffPath;						// stored diff file path so closing this floater doesn't reset it
 
 	// Internal functionality
-	static void popupAndPrintWarning(std::string& warning);			// pop up a warning
+	static void popupAndPrintWarning(const std::string& warning);	// pop up a warning
 	std::string getLocalizedDirectory();							// build and return the path to the XUI directory for the currently-selected localization
 	void scanDiffFile(LLXmlTreeNode* file_node);					// scan a given XML node for diff entries and highlight them in its associated file
 	void highlightChangedElements();								// look up the list of elements to highlight and highlight them in the current floater
@@ -597,7 +600,7 @@ void LLFloaterUIPreview::onClose(bool app_quitting)
 
 // Error handling (to avoid code repetition)
 // *TODO: this is currently unlocalized.  Add to alerts/notifications.xml, someday, maybe.
-void LLFloaterUIPreview::popupAndPrintWarning(std::string& warning)
+void LLFloaterUIPreview::popupAndPrintWarning(const std::string& warning)
 {
 	llwarns << warning << llendl;
 	LLSD args;
@@ -998,190 +1001,55 @@ void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID, bool save)
 // Respond to button click to edit currently-selected floater
 void LLFloaterUIPreview::onClickEditFloater()
 {
-	std::string file_name = mFileList->getSelectedItemLabel(1);	// get the file name of the currently-selected floater
-	if(std::string("") == file_name)										// if no item is selected
-	{
-		return;															// ignore click
-	}
-	std::string path = getLocalizedDirectory() + file_name;
-
-	// stat file to see if it exists (some localized versions may not have it there are no diffs, and then we try to open an nonexistent file)
-	llstat dummy;
-	if(LLFile::stat(path.c_str(), &dummy))								// if the file does not exist
-	{
-		std::string warning = "No file for this floater exists in the selected localization.  Opening the EN version instead.";
-		popupAndPrintWarning(warning);
-
-		path = get_xui_dir() + mDelim + "en" + mDelim + file_name; // open the en version instead, by default
-	}
-
-	// get executable path
-	const char* exe_path_char;
-	std::string path_in_textfield = mEditorPathTextBox->getText();
-	if(std::string("") != path_in_textfield)	// if the text field is not emtpy, use its path
-	{
-		exe_path_char = path_in_textfield.c_str();
-	}
-	else if (!LLUI::sSettingGroups["config"]->getString("XUIEditor").empty())
-	{
-		exe_path_char = LLUI::sSettingGroups["config"]->getString("XUIEditor").c_str();
-	}
-	else									// otherwise use the path specified by the environment variable
+	// Determine file to edit.
+	std::string file_path;
 	{
-		exe_path_char = getenv("LL_XUI_EDITOR");
-	}
-
-	// error check executable path
-	if(NULL == exe_path_char)
-	{
-		std::string warning = "Select an editor by setting the environment variable LL_XUI_EDITOR or specifying its path in the \"Editor Path\" field.";
-		popupAndPrintWarning(warning);
-		return;
-	}
-	std::string exe_path = exe_path_char;	// do this after error check, otherwise internal strlen call fails on bad char*
-
-	// remove any quotes; they're added back in later where necessary
-	int found_at;
-	while((found_at = exe_path.find("\"")) != -1 || (found_at = exe_path.find("'")) != -1)
-	{
-		exe_path.erase(found_at,1);
-	}
-
-	llstat s;
-	if(!LLFile::stat(exe_path.c_str(), &s)) // If the executable exists
-	{
-		// build paths and arguments
-		std::string quote = std::string("\"");
-		std::string args;
-		std::string custom_args = mEditorArgsTextBox->getText();
-		int position_of_file = custom_args.find(std::string("%FILE%"), 0);	// prepare to replace %FILE% with actual file path
-		std::string first_part_of_args = "";
-		std::string second_part_of_args = "";
-		if(-1 == position_of_file)	// default: Executable.exe File.xml
-		{
-			args = quote + path + quote;			// execute the command Program.exe "File.xml"
-		}
-		else						// use advanced command-line arguments, e.g. "Program.exe -safe File.xml" -windowed for "-safe %FILE% -windowed"
+		std::string file_name = mFileList->getSelectedItemLabel(1);	// get the file name of the currently-selected floater
+		if (file_name.empty())					// if no item is selected
 		{
-			first_part_of_args = custom_args.substr(0,position_of_file);											// get part of args before file name
-			second_part_of_args = custom_args.substr(position_of_file+6,custom_args.length());						// get part of args after file name
-			custom_args = first_part_of_args + std::string("\"") + path + std::string("\"") + second_part_of_args;	// replace %FILE% with "<file path>" and put back together
-			args = custom_args;																						// and save in the variable that is actually used
+			llwarns << "No file selected" << llendl;
+			return;															// ignore click
 		}
+		file_path = getLocalizedDirectory() + file_name;
 
-		// find directory in which executable resides by taking everything after last slash
-		int last_slash_position = exe_path.find_last_of(mDelim);
-		if(-1 == last_slash_position)
-		{
-			std::string warning = std::string("Unable to find a valid path to the specified executable for XUI XML editing: ") + exe_path;
-			popupAndPrintWarning(warning);
-			return;
-		}
-        std::string exe_dir = exe_path.substr(0,last_slash_position); // strip executable off, e.g. get "C:\Program Files\TextPad 5" (with or without trailing slash)
-
-#if LL_WINDOWS
-		PROCESS_INFORMATION pinfo;
-		STARTUPINFOA sinfo;
-		memset(&sinfo, 0, sizeof(sinfo));
-		memset(&pinfo, 0, sizeof(pinfo));
-
-		std::string exe_name = exe_path.substr(last_slash_position+1);
-		args = quote + exe_name + quote + std::string(" ") + args;				// and prepend the executable name, so we get 'Program.exe "Arg1"'
-
-		char *args2 = new char[args.size() + 1];	// Windows requires that the second parameter to CreateProcessA be a writable (non-const) string...
-		strcpy(args2, args.c_str());
-
-		// we don't want the current directory to be the executable directory, since the file path is now relative. By using
-		// NULL for the current directory instead of exe_dir.c_str(), the path to the target file will work. 
-		if(!CreateProcessA(exe_path.c_str(), args2, NULL, NULL, FALSE, 0, NULL, NULL, &sinfo, &pinfo))
-		{
-			// DWORD dwErr = GetLastError();
-			std::string warning = "Creating editor process failed!";
-			popupAndPrintWarning(warning);
-		}
-		else
+		// stat file to see if it exists (some localized versions may not have it there are no diffs, and then we try to open an nonexistent file)
+		llstat dummy;
+		if(LLFile::stat(file_path.c_str(), &dummy))								// if the file does not exist
 		{
-			// foo = pinfo.dwProcessId; // get your pid here if you want to use it later on
-			// sGatewayHandle = pinfo.hProcess;
-			CloseHandle(pinfo.hThread); // stops leaks - nothing else
+			popupAndPrintWarning("No file for this floater exists in the selected localization.  Opening the EN version instead.");
+			file_path = get_xui_dir() + mDelim + "en" + mDelim + file_name; // open the en version instead, by default
 		}
+	}
 
-		delete[] args2;
-#else	// if !LL_WINDOWS
-		// This code was copied from the code to run SLVoice, with some modification; should work in UNIX (Mac/Darwin or Linux)
+	// Set the editor command.
+	std::string cmd_override;
+	{
+		std::string bin = mEditorPathTextBox->getText();
+		if (!bin.empty())
 		{
-			std::vector<std::string> arglist;
-			arglist.push_back(exe_path.c_str());
-
-			// Split the argument string into separate strings for each argument
-			typedef boost::tokenizer< boost::char_separator<char> > tokenizer;
-			boost::char_separator<char> sep("","\" ", boost::drop_empty_tokens);
-
-			tokenizer tokens(args, sep);
-			tokenizer::iterator token_iter;
-			BOOL inside_quotes = FALSE;
-			BOOL last_was_space = FALSE;
-			for(token_iter = tokens.begin(); token_iter != tokens.end(); ++token_iter)
-			{
-				if(!strncmp("\"",(*token_iter).c_str(),2))
-				{
-					inside_quotes = !inside_quotes;
-				}
-				else if(!strncmp(" ",(*token_iter).c_str(),2))
-				{
-					if(inside_quotes)
-					{
-						arglist.back().append(std::string(" "));
-						last_was_space = TRUE;
-					}
-				}
-				else
-				{
-					std::string to_push = *token_iter;
-					if(last_was_space)
-					{
-						arglist.back().append(to_push);
-						last_was_space = FALSE;
-					}
-					else
-					{
-						arglist.push_back(to_push);
-					}
-				}
-			}
-			
-			// create an argv vector for the child process
-			char **fakeargv = new char*[arglist.size() + 1];
-			int i;
-			for(i=0; i < arglist.size(); i++)
-				fakeargv[i] = const_cast<char*>(arglist[i].c_str());
-
-			fakeargv[i] = NULL;
-
-			fflush(NULL); // flush all buffers before the child inherits them
-			pid_t id = vfork();
-			if(id == 0)
+			// surround command with double quotes for the case if the path contains spaces
+			if (bin.find("\"") == std::string::npos)
 			{
-				// child
-				execv(exe_path.c_str(), fakeargv);
-
-				// If we reach this point, the exec failed.
-				// Use _exit() instead of exit() per the vfork man page.
-				std::string warning = "Creating editor process failed (vfork/execv)!";
-				popupAndPrintWarning(warning);
-				_exit(0);
+				bin = "\"" + bin + "\"";
 			}
 
-			// parent
-			delete[] fakeargv;
-			// sGatewayPID = id;
+			std::string args = mEditorArgsTextBox->getText();
+			cmd_override = bin + " " + args;
 		}
-#endif	// LL_WINDOWS
 	}
-	else
+	if (!mExternalEditor.setCommand("LL_XUI_EDITOR", cmd_override))
 	{
-		std::string warning = "Unable to find path to external XML editor for XUI preview tool";
+		std::string warning = "Select an editor by setting the environment variable LL_XUI_EDITOR "
+			"or the ExternalEditor setting or specifying its path in the \"Editor Path\" field.";
 		popupAndPrintWarning(warning);
+		return;
+	}
+
+	// Run the editor.
+	if (!mExternalEditor.run(file_path))
+	{
+		popupAndPrintWarning("Failed to run editor");
+		return;
 	}
 }
 
diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp
index cf2ea382880..330e809c539 100644
--- a/indra/newview/llpreviewscript.cpp
+++ b/indra/newview/llpreviewscript.cpp
@@ -34,11 +34,13 @@
 #include "llcheckboxctrl.h"
 #include "llcombobox.h"
 #include "lldir.h"
+#include "llexternaleditor.h"
 #include "llfloaterreg.h"
 #include "llinventorydefines.h"
 #include "llinventorymodel.h"
 #include "llkeyboard.h"
 #include "lllineeditor.h"
+#include "lllivefile.h"
 #include "llhelp.h"
 #include "llnotificationsutil.h"
 #include "llresmgr.h"
@@ -115,6 +117,54 @@ static bool have_script_upload_cap(LLUUID& object_id)
 	return object && (! object->getRegion()->getCapability("UpdateScriptTask").empty());
 }
 
+/// ---------------------------------------------------------------------------
+/// LLLiveLSLFile
+/// ---------------------------------------------------------------------------
+class LLLiveLSLFile : public LLLiveFile
+{
+public:
+	LLLiveLSLFile(std::string file_path, LLLiveLSLEditor* parent);
+	~LLLiveLSLFile();
+
+	void ignoreNextUpdate() { mIgnoreNextUpdate = true; }
+
+protected:
+	/*virtual*/ bool loadFile();
+
+	LLLiveLSLEditor*	mParent;
+	bool				mIgnoreNextUpdate;
+};
+
+LLLiveLSLFile::LLLiveLSLFile(std::string file_path, LLLiveLSLEditor* parent)
+:	mParent(parent)
+,	mIgnoreNextUpdate(false)
+,	LLLiveFile(file_path, 1.0)
+{
+}
+
+LLLiveLSLFile::~LLLiveLSLFile()
+{
+	LLFile::remove(filename());
+}
+
+bool LLLiveLSLFile::loadFile()
+{
+	if (mIgnoreNextUpdate)
+	{
+		mIgnoreNextUpdate = false;
+		return true;
+	}
+
+	if (!mParent->loadScriptText(filename()))
+	{
+		return false;
+	}
+
+	// Disable sync to avoid recursive load->save->load calls.
+	mParent->saveIfNeeded(false);
+	return true;
+}
+
 /// ---------------------------------------------------------------------------
 /// LLFloaterScriptSearch
 /// ---------------------------------------------------------------------------
@@ -281,6 +331,7 @@ LLScriptEdCore::LLScriptEdCore(
 	const LLHandle<LLFloater>& floater_handle,
 	void (*load_callback)(void*),
 	void (*save_callback)(void*, BOOL),
+	void (*edit_callback)(void*),
 	void (*search_replace_callback) (void* userdata),
 	void* userdata,
 	S32 bottom_pad)
@@ -290,6 +341,7 @@ LLScriptEdCore::LLScriptEdCore(
 	mEditor( NULL ),
 	mLoadCallback( load_callback ),
 	mSaveCallback( save_callback ),
+	mEditCallback( edit_callback ),
 	mSearchReplaceCallback( search_replace_callback ),
 	mUserdata( userdata ),
 	mForceClose( FALSE ),
@@ -329,6 +381,7 @@ BOOL LLScriptEdCore::postBuild()
 
 	childSetCommitCallback("lsl errors", &LLScriptEdCore::onErrorList, this);
 	childSetAction("Save_btn", boost::bind(&LLScriptEdCore::doSave,this,FALSE));
+	childSetAction("Edit_btn", boost::bind(&LLScriptEdCore::onEditButtonClick, this));
 
 	initMenu();
 
@@ -809,6 +862,13 @@ void LLScriptEdCore::doSave( BOOL close_after_save )
 	}
 }
 
+void LLScriptEdCore::onEditButtonClick()
+{
+	if (mEditCallback)
+	{
+		mEditCallback(mUserdata);
+	}
+}
 
 void LLScriptEdCore::onBtnUndoChanges()
 {
@@ -949,6 +1009,7 @@ void* LLPreviewLSL::createScriptEdPanel(void* userdata)
 								   self->getHandle(),
 								   LLPreviewLSL::onLoad,
 								   LLPreviewLSL::onSave,
+								   NULL, // no edit callback
 								   LLPreviewLSL::onSearchReplace,
 								   self,
 								   0);
@@ -1417,6 +1478,7 @@ void* LLLiveLSLEditor::createScriptEdPanel(void* userdata)
 								   self->getHandle(),
 								   &LLLiveLSLEditor::onLoad,
 								   &LLLiveLSLEditor::onSave,
+								   &LLLiveLSLEditor::onEdit,
 								   &LLLiveLSLEditor::onSearchReplace,
 								   self,
 								   0);
@@ -1433,6 +1495,7 @@ LLLiveLSLEditor::LLLiveLSLEditor(const LLSD& key) :
 	mCloseAfterSave(FALSE),
 	mPendingUploads(0),
 	mIsModifiable(FALSE),
+	mLiveFile(NULL),
 	mIsNew(false)
 {
 	mFactoryMap["script ed panel"] = LLCallbackMap(LLLiveLSLEditor::createScriptEdPanel, this);
@@ -1458,6 +1521,7 @@ BOOL LLLiveLSLEditor::postBuild()
 
 LLLiveLSLEditor::~LLLiveLSLEditor()
 {
+	delete mLiveFile;
 }
 
 // virtual
@@ -1639,38 +1703,39 @@ void LLLiveLSLEditor::onLoadComplete(LLVFS *vfs, const LLUUID& asset_id,
 	delete xored_id;
 }
 
-// unused
-// void LLLiveLSLEditor::loadScriptText(const std::string& filename)
-// {
-// 	if(!filename)
-// 	{
-// 		llerrs << "Filename is Empty!" << llendl;
-// 		return;
-// 	}
-// 	LLFILE* file = LLFile::fopen(filename, "rb");		/*Flawfinder: ignore*/
-// 	if(file)
-// 	{
-// 		// read in the whole file
-// 		fseek(file, 0L, SEEK_END);
-// 		long file_length = ftell(file);
-// 		fseek(file, 0L, SEEK_SET);
-// 		char* buffer = new char[file_length+1];
-// 		size_t nread = fread(buffer, 1, file_length, file);
-// 		if (nread < (size_t) file_length)
-// 		{
-// 			llwarns << "Short read" << llendl;
-// 		}
-// 		buffer[nread] = '\0';
-// 		fclose(file);
-// 		mScriptEd->mEditor->setText(LLStringExplicit(buffer));
-// 		mScriptEd->mEditor->makePristine();
-// 		delete[] buffer;
-// 	}
-// 	else
-// 	{
-// 		llwarns << "Error opening " << filename << llendl;
-// 	}
-// }
+ bool LLLiveLSLEditor::loadScriptText(const std::string& filename)
+ {
+ 	if (filename.empty())
+ 	{
+ 		llwarns << "Empty file name" << llendl;
+ 		return false;
+ 	}
+
+ 	LLFILE* file = LLFile::fopen(filename, "rb");		/*Flawfinder: ignore*/
+ 	if (!file)
+ 	{
+ 		llwarns << "Error opening " << filename << llendl;
+ 		return false;
+ 	}
+
+ 	// read in the whole file
+	fseek(file, 0L, SEEK_END);
+	size_t file_length = (size_t) ftell(file);
+	fseek(file, 0L, SEEK_SET);
+	char* buffer = new char[file_length+1];
+	size_t nread = fread(buffer, 1, file_length, file);
+	if (nread < file_length)
+	{
+		llwarns << "Short read" << llendl;
+	}
+	buffer[nread] = '\0';
+	fclose(file);
+	mScriptEd->mEditor->setText(LLStringExplicit(buffer));
+	//mScriptEd->mEditor->makePristine();
+	delete[] buffer;
+
+	return true;
+ }
 
 void LLLiveLSLEditor::loadScriptText(LLVFS *vfs, const LLUUID &uuid, LLAssetType::EType type)
 {
@@ -1825,9 +1890,8 @@ LLLiveLSLSaveData::LLLiveLSLSaveData(const LLUUID& id,
 	mItem = new LLViewerInventoryItem(item);
 }
 
-void LLLiveLSLEditor::saveIfNeeded()
+void LLLiveLSLEditor::saveIfNeeded(bool sync)
 {
-	llinfos << "LLLiveLSLEditor::saveIfNeeded()" << llendl;
 	LLViewerObject* object = gObjectList.findObject(mObjectUUID);
 	if(!object)
 	{
@@ -1877,9 +1941,74 @@ void LLLiveLSLEditor::saveIfNeeded()
 	mItem->setAssetUUID(asset_id);
 	mItem->setTransactionID(tid);
 
-	// write out the data, and store it in the asset database
+	writeToFile(filename);
+
+	if (sync)
+	{
+		// Sync with external ed2itor.
+		std::string tmp_file = getTmpFileName();
+		llstat s;
+		if (LLFile::stat(tmp_file, &s) == 0) // file exists
+		{
+			if (mLiveFile) mLiveFile->ignoreNextUpdate();
+			writeToFile(tmp_file);
+		}
+	}
+	
+	// save it out to asset server
+	std::string url = object->getRegion()->getCapability("UpdateScriptTask");
+	getWindow()->incBusyCount();
+	mPendingUploads++;
+	BOOL is_running = getChild<LLCheckBoxCtrl>( "running")->get();
+	if (!url.empty())
+	{
+		uploadAssetViaCaps(url, filename, mObjectUUID, mItemUUID, is_running);
+	}
+	else if (gAssetStorage)
+	{
+		uploadAssetLegacy(filename, object, tid, is_running);
+	}
+}
+
+void LLLiveLSLEditor::openExternalEditor()
+{
+	LLViewerObject* object = gObjectList.findObject(mObjectUUID);
+	if(!object)
+	{
+		LLNotificationsUtil::add("SaveScriptFailObjectNotFound");
+		return;
+	}
+
+	delete mLiveFile; // deletes file
+
+	// Save the script to a temporary file.
+	std::string filename = getTmpFileName();
+	writeToFile(filename);
+
+	// Start watching file changes.
+	mLiveFile = new LLLiveLSLFile(filename, this);
+	mLiveFile->addToEventTimer();
+
+	// Open it in external editor.
+	{
+		LLExternalEditor ed;
+
+		if (!ed.setCommand("LL_SCRIPT_EDITOR"))
+		{
+			std::string msg = "Select an editor by setting the environment variable LL_SCRIPT_EDITOR "
+				"or the ExternalEditor setting"; // *TODO: localize
+			LLNotificationsUtil::add("GenericAlert", LLSD().with("MESSAGE", msg));
+			return;
+		}
+
+		ed.run(filename);
+	}
+}
+
+bool LLLiveLSLEditor::writeToFile(const std::string& filename)
+{
 	LLFILE* fp = LLFile::fopen(filename, "wb");
-	if(!fp)
+	if (!fp)
 	{
 		llwarns << "Unable to write to " << filename << llendl;
 
@@ -1887,33 +2016,25 @@ void LLLiveLSLEditor::saveIfNeeded()
 		row["columns"][0]["value"] = "Error writing to local file. Is your hard drive full?";
 		row["columns"][0]["font"] = "SANSSERIF_SMALL";
 		mScriptEd->mErrorList->addElement(row);
-		return;
+		return false;
 	}
+
 	std::string utf8text = mScriptEd->mEditor->getText();
 
 	// Special case for a completely empty script - stuff in one space so it can store properly.  See SL-46889
-	if ( utf8text.size() == 0 )
+	if (utf8text.size() == 0)
 	{
 		utf8text = " ";
 	}
 
 	fputs(utf8text.c_str(), fp);
 	fclose(fp);
-	fp = NULL;
-	
-	// save it out to asset server
-	std::string url = object->getRegion()->getCapability("UpdateScriptTask");
-	getWindow()->incBusyCount();
-	mPendingUploads++;
-	BOOL is_running = getChild<LLCheckBoxCtrl>( "running")->get();
-	if (!url.empty())
-	{
-		uploadAssetViaCaps(url, filename, mObjectUUID, mItemUUID, is_running);
-	}
-	else if (gAssetStorage)
-	{
-		uploadAssetLegacy(filename, object, tid, is_running);
-	}
+	return true;
+}
+
+std::string LLLiveLSLEditor::getTmpFileName()
+{
+	return std::string(LLFile::tmpdir()) + "sl_script_" + mObjectUUID.asString() + ".lsl";
 }
 
 void LLLiveLSLEditor::uploadAssetViaCaps(const std::string& url,
@@ -2138,6 +2259,14 @@ void LLLiveLSLEditor::onSave(void* userdata, BOOL close_after_save)
 	self->saveIfNeeded();
 }
 
+
+// static
+void LLLiveLSLEditor::onEdit(void* userdata)
+{
+	LLLiveLSLEditor* self = (LLLiveLSLEditor*)userdata;
+	self->openExternalEditor();
+}
+
 // static
 void LLLiveLSLEditor::processScriptRunningReply(LLMessageSystem* msg, void**)
 {
diff --git a/indra/newview/llpreviewscript.h b/indra/newview/llpreviewscript.h
index f4b31e5962f..d35c6b85283 100644
--- a/indra/newview/llpreviewscript.h
+++ b/indra/newview/llpreviewscript.h
@@ -35,6 +35,7 @@
 #include "lliconctrl.h"
 #include "llframetimer.h"
 
+class LLLiveLSLFile;
 class LLMessageSystem;
 class LLTextEditor;
 class LLButton;
@@ -62,6 +63,7 @@ class LLScriptEdCore : public LLPanel
 		const LLHandle<LLFloater>& floater_handle,
 		void (*load_callback)(void* userdata),
 		void (*save_callback)(void* userdata, BOOL close_after_save),
+		void (*edit_callback)(void*),
 		void (*search_replace_callback)(void* userdata),
 		void* userdata,
 		S32 bottom_pad = 0);	// pad below bottom row of buttons
@@ -80,6 +82,8 @@ class LLScriptEdCore : public LLPanel
 	bool			handleSaveChangesDialog(const LLSD& notification, const LLSD& response);
 	bool			handleReloadFromServerDialog(const LLSD& notification, const LLSD& response);
 
+	void			onEditButtonClick();
+
 	static void		onCheckLock(LLUICtrl*, void*);
 	static void		onHelpComboCommit(LLUICtrl* ctrl, void* userdata);
 	static void		onClickBack(void* userdata);
@@ -114,6 +118,7 @@ class LLScriptEdCore : public LLPanel
 	LLTextEditor*	mEditor;
 	void			(*mLoadCallback)(void* userdata);
 	void			(*mSaveCallback)(void* userdata, BOOL close_after_save);
+	void			(*mEditCallback)(void* userdata);
 	void			(*mSearchReplaceCallback) (void* userdata);
 	void*			mUserdata;
 	LLComboBox		*mFunctions;
@@ -179,6 +184,7 @@ class LLPreviewLSL : public LLPreview
 // Used to view and edit an LSL that is attached to an object.
 class LLLiveLSLEditor : public LLPreview
 {
+	friend class LLLiveLSLFile;
 public: 
 	LLLiveLSLEditor(const LLSD& key);
 	~LLLiveLSLEditor();
@@ -202,7 +208,10 @@ class LLLiveLSLEditor : public LLPreview
 
 	virtual void loadAsset();
 	void loadAsset(BOOL is_new);
-	void saveIfNeeded();
+	void saveIfNeeded(bool sync = true);
+	void openExternalEditor();
+	std::string getTmpFileName();
+	bool writeToFile(const std::string& filename);
 	void uploadAssetViaCaps(const std::string& url,
 							const std::string& filename, 
 							const LLUUID& task_id,
@@ -218,6 +227,7 @@ class LLLiveLSLEditor : public LLPreview
 	static void onSearchReplace(void* userdata);
 	static void onLoad(void* userdata);
 	static void onSave(void* userdata, BOOL close_after_save);
+	static void onEdit(void* userdata);
 
 	static void onLoadComplete(LLVFS *vfs, const LLUUID& asset_uuid,
 							   LLAssetType::EType type,
@@ -227,7 +237,7 @@ class LLLiveLSLEditor : public LLPreview
 	static void onRunningCheckboxClicked(LLUICtrl*, void* userdata);
 	static void onReset(void* userdata);
 
-// 	void loadScriptText(const std::string& filename); // unused
+ 	bool loadScriptText(const std::string& filename);
 	void loadScriptText(LLVFS *vfs, const LLUUID &uuid, LLAssetType::EType type);
 
 	static void onErrorList(LLUICtrl*, void* user_data);
@@ -253,6 +263,7 @@ class LLLiveLSLEditor : public LLPreview
 	
 	LLCheckBoxCtrl*	mMonoCheckbox;
 	BOOL mIsModifiable;
+	LLLiveLSLFile*		mLiveFile;
 };
 
 #endif  // LL_LLPREVIEWSCRIPT_H
diff --git a/indra/newview/skins/default/xui/en/panel_script_ed.xml b/indra/newview/skins/default/xui/en/panel_script_ed.xml
index 1e332a40c2e..a041c9b2293 100644
--- a/indra/newview/skins/default/xui/en/panel_script_ed.xml
+++ b/indra/newview/skins/default/xui/en/panel_script_ed.xml
@@ -179,4 +179,13 @@
      right="487"
      name="Save_btn"
      width="81" />
+    <button
+     follows="right|bottom"
+     height="23"
+     label="Edit..."
+     layout="topleft"
+     top_pad="-23"
+     right="400"
+     name="Edit_btn"
+     width="81" />
 </panel>
-- 
GitLab