Opensource Script Editor

As soon as I can finish up some things I’ll be releasing this new script editor I’m working on. Please see snapshot at the bottom for what I have so far…
Features :

  • syntax highlighting
  • reads all script types - .shader, .cs, .js, .boo, .txt, .ini, etc…
  • saving, loading, and creating of new scripts
  • more to come :smile:

The great thing about this new editor is you no longer have to leave unity to code any more :smile:

Release notes

  • Version 1.0

  • Load, Create and Save features added

  • Color coding of text added

  • Basic syntax highlighting

  • Version 1.0.1

  • Menu bar added

  • Support for multiple files open at once added

Bugs List

  • Comments and Strings syntax highlights will sometimes continue onto the next line
  • Comments containing ‘/* … */’ Don’t highlight correctly at the end;
  • Text Editor Doesn’t refresh when you open a file or switch between them. To refresh just click back inside the textfield.
  • Line numbering doesn’t take into account wrapping text.
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class ScriptEditor : EditorWindow 
{
	string s = "";
	int lines = 0;
	string path = Application.dataPath;
	Vector2 scrollPos = new Vector2 ( );	

	GUIStyle textStyle = new GUIStyle();	
	Vector2 tSize = new Vector2();

	bool isCommenting = false;
	bool isString = false;
	bool commentAffectsNextLine = false;

	List<string> redos = new List<string> ( );

	int toolbarMenuIndex = -1;
	string[] fileOptions = new string [ ]
			{
				"New",
				"Load",
				"Save",
                                "Save as",
				"Close Tab",
				"Close All Tabs"
			};

	string[] editOptions = new string [ ]
			{
				"Cut",
				"Copy",				
				"Paste"
			};

	#region cSharp namespaces
	public List<string> cNamespaces = new List<string> ( )
	{
		"abstract",
		"as",
		"base",
		"break",		
		"bool",
		"by",
		"byte",
		"case",
		"catch",
		"char",
		"checked",
		"class",
		"const",
		"continue",
		"decimal",
		"default",
		"delegate",
		"do",
		"double",
		"else",
		"enum",
		"event",
		"explicit",
		"extern",
		"false",	
		"finally",
		"fixed",
		"float",
		"for",
		"foreach",
		"from",
		"group",
		"if",
		"implicit",
		"in",		
		"int",
		"interface",
		"internal",
		"into",
		"is",
		"lock",
		"long",
		"namespace",
		"new",
		"null",
		"object",
		"operator",
		"out",
		"override",
		"params",
		"private",
		"protected",
		"public",
		"readonly",
		"ref",
		"return",
		"sbyte",
		"sealed",
		"select",
		"sizeof",
		"short",
		"stackalloc",
		"static",
		"string",
		"struct",
		"switch",
		"this",
		"throw",
		"true",
		"try",
		"typeof",
		"unchecked",
		"uint",
		"ulong",
		"unsafe",
		"using",
		"var",
		"virtual",
		"volatile",
		"void",
		"where",
		"while",
		"yield"
	};
	#endregion

	List<Tab> fileTabs = new List<Tab> ( );
	int fileTabIndex = -1;
	// Use this for initialization
	[MenuItem ( "Window/Script Editor" )]
	static void Init ( )
	{
		ScriptEditor window = ( ScriptEditor )EditorWindow.GetWindow ( typeof ( ScriptEditor ), false, "Script Editor" );
		window.s= "";
                window.lines = 0;
		window.textStyle = null;
		window.minSize = new Vector2 ( 200, 100 );
		window.cNamespaces = new List<string> ( )
		{
			"abstract",
			"as",
			"base",
			"break",		
			"bool",
			"by",
			"byte",
			"case",
			"catch",
			"char",
			"checked",
			"class",
			"const",
			"continue",
			"decimal",
			"default",
			"delegate",
			"do",
			"double",
			"else",
			"enum",
			"event",
			"explicit",
			"extern",
			"false",	
			"finally",
			"fixed",
			"float",
			"for",
			"foreach",
			"from",
			"group",
			"if",
			"implicit",
			"in",		
			"int",
			"interface",
			"internal",
			"into",
			"is",
			"lock",
			"long",
			"namespace",
			"new",
			"null",
			"object",
			"operator",
			"out",
			"override",
			"params",
			"private",
			"protected",
			"public",
			"readonly",
			"ref",
			"return",
			"sbyte",
			"sealed",
			"select",
			"sizeof",
			"short",
			"stackalloc",
			"static",
			"string",
			"struct",
			"switch",
			"this",
			"throw",
			"true",
			"try",
			"typeof",
			"unchecked",
			"uint",
			"ulong",
			"unsafe",
			"using",
			"var",
			"virtual",
			"volatile",
			"void",
			"where",
			"while",
			"yield"
		};
	}

	class Tab
	{
		public string fileName;
		public string filePath;
		public string fileText;

		public Tab (string name, string path, string text )
		{
			fileName = name;
			filePath = path;
			fileText = text;
		}
	}
	
	// Update is called once per frame
	void OnGUI ()
	{
                if(textStyle == null)
			textStyle = new GUIStyle ( GUI.skin.textArea );
		TextEditor te = ( TextEditor )GUIUtility.GetStateObject ( typeof ( TextEditor ), GUIUtility.keyboardControl );
		//backup color 
		Color backupColor = Color.white;
		Color backupContentColor = Color.black;
		Color backupBackgroundColor = GUI.backgroundColor;

		GUI.Box ( new Rect ( 0, 0, position.width, 20 ), "", "toolbar" );

		toolbarMenuIndex = EditorGUI.Popup ( new Rect ( 0, 0, 40, 20 ), toolbarMenuIndex, fileOptions, "toolbarButton" );
		GUI.Label ( new Rect ( 0, 0, 40, 20 ), "File" );
		if ( GUI.changed )
		{
			CheckFileOptions (te );
		}

		toolbarMenuIndex = EditorGUI.Popup ( new Rect ( 40, 0, 40, 20 ), toolbarMenuIndex, editOptions, "toolbarButton" );
		GUI.Label ( new Rect ( 40, 0, 40, 20 ), "Edit" );
		if ( GUI.changed )
		{
			CheckEditOptions ( te );
		}
		GUI.Box ( new Rect ( 0, 18, position.width, 20 ), "", "toolbar" );

		tSize.y =  textStyle.CalcHeight ( new GUIContent ( s ), position.width );
		tSize.x = position.width;

		GUILayout.BeginArea (new Rect(0,18,position.width,20) );
		GUILayout.BeginHorizontal ( );
		if(fileTabs.Count > 0)
		{
			for(int i = 0; i < fileTabs.Count; i++)
			{
				Tab tab = fileTabs[i];
				if ( GUILayout.Button ( tab.fileName, "toolbarButton" ) )
				{
					fileTabs[fileTabIndex].fileText = s;
					fileTabIndex = i;
					s = tab.fileText;
				}				
			}			
		}
		GUILayout.EndHorizontal ( );
		GUILayout.EndArea ( );
		scrollPos = GUI.BeginScrollView ( new Rect(0,36,position.width, position.height-36), scrollPos, new Rect(0,0,tSize.x-15,tSize.y+20) );
		
		for ( int i = 0; i < lines; i++ )
		{
			Vector2 lSize = textStyle.CalcSize ( new GUIContent ( i.ToString ( ) ) );
			if(i%2 == 1)
			{
				GUI.color = Color.gray;
				GUI.Box (new Rect(-2, 13 * i+13, position.width+4 + tSize.x, 13 ), "");
			}
			GUI.color = Color.white;
			GUI.Label ( new Rect ( 0, 13 * i+13, lSize.x + 50, 13 ), "" + i );
		}

		Event ev = Event.current;

		//add textarea with transparent text
		GUI.contentColor = new Color ( 1f, 1f, 1f, 0f );
		Rect bounds = new Rect ( 60, 13, position.width-80, position.height+tSize.y);
		GUI.SetNextControlName ( "TextArea" );

		s = GUI.TextArea ( bounds, s );
		//get the texteditor of the textarea to control selection
		

		CheckKeys ( te, ev );
		//set background of all textfield transparent
		GUI.backgroundColor = new Color ( 1f, 1f, 1f, 0f );

		//backup selection to remake it after process
		int backupPos = te.pos;
		int backupSelPos = te.selectPos;

		//get last position in text
		te.MoveTextEnd ( );
		int endpos = te.pos;
		//draw textfield with color on top of text area
		UpdateText ( te, ev, endpos, textStyle, backupPos, backupSelPos );

		//Reset color
		GUI.color = backupColor;
		GUI.contentColor = backupContentColor;
		GUI.backgroundColor = backupBackgroundColor;

		GUI.EndScrollView ( );
	}

	private void CheckFileOptions (TextEditor te )
	{
		FileStream fs;
		StreamWriter sw;
		string path;
		if ( toolbarMenuIndex == -1 )
			return;
		switch ( toolbarMenuIndex )
		{
			//New
			case 0:
				this.path = "";
				textStyle = new GUIStyle ( GUI.skin.textArea );
				s = "";
				redos.Clear ( );
				break;

			//Open
			case 1:
				path = EditorUtility.OpenFilePanel ( "Load...", "", "*.*" );
				if ( path != "" )
				{
					this.path = path;
									
					textStyle = new GUIStyle ( GUI.skin.textArea );
					bool fileIsOpen = false;
					if ( fileTabs.Count > 0 )
					{
						for ( int i = 0; i < fileTabs.Count; i++ )
						{
							Tab t = fileTabs [ i ];
							if ( t.filePath  == path )
							{
								fileIsOpen = true;
							}
						}
						if ( !fileIsOpen )
						{
							s = "";
							lines = 0;
							LoadFile ( path );
						}
					}
					else
					{
						s="";
						lines = 0;
						LoadFile (path );
					}

				}
				redos.Clear ( );
				
				break;

			//Save
			case 2:
				fs = new FileStream ( this.path, FileMode.Open );
				fs.SetLength ( 0 );
				fs.Close ( );
				sw = new StreamWriter ( this.path );
				sw.Write ( s );
				sw.Close ( );
				AssetDatabase.Refresh ( );
				break;

			//Save as
			case 3:
				path = EditorUtility.OpenFilePanel ( "Save as...", "", "*.*" );
				if ( path != "" )
				{
					this.path = path;
					fs = new FileStream ( path, FileMode.Open );
					fs.SetLength ( 0 );
					fs.Close ( );
					sw = new StreamWriter ( path );
					sw.Write ( s );
					sw.Close ( );
					AssetDatabase.Refresh ( );
				}
				break;

			//Close
			case 4:

				break;

			//Close all
			case 5:

				break;
		}
		toolbarMenuIndex = -1;
	}

	private void LoadFile (string path)
	{
		StreamReader sr = new StreamReader ( path );
		string newText = "";
		while ( !sr.EndOfStream )
		{
			string newLine = sr.ReadLine ( );
			newText += newLine + "\n";
		}
		int index = path.LastIndexOf ( "/" );
		string file = path.Substring ( index+1);
		sr.Close ( );
		fileTabs.Add ( new Tab ( file, path, newText ) );
		fileTabIndex = fileTabs.Count-1;
		s = newText;
		tSize.y =  textStyle.CalcHeight ( new GUIContent ( s ), position.width );
		tSize.x = position.width;
		Repaint ( );
	}

	private void CheckEditOptions (TextEditor te )
	{
		if ( toolbarMenuIndex == -1 )
			return;

		switch ( toolbarMenuIndex )
		{
			//Cut
			case 0:
				te.Cut();
				s = te.content.text;
				break;

			//Copy
			case 1:
				te.Copy();
				break;

			//Paste
			case 2:
				te.Paste();
				s = te.content.text;
				break;
		}

		toolbarMenuIndex = -1;
	}

	void UpdateText (TextEditor te, Event ev, int endpos, GUIStyle textStyle, int backupPos, int backupSelPos )
	{
		te.MoveTextStart ( );
		lines = 0;
		while ( te.pos != endpos )
		{
			te.SelectToStartOfNextWord ( );
			string wordtext = te.SelectedText;

			//set word color
			GUI.contentColor = CheckSyntax ( wordtext );

			Vector2 pixelselpos = textStyle.GetCursorPixelPosition ( te.position, te.content, te.selectPos );
			Vector2 pixelpos = textStyle.GetCursorPixelPosition ( te.position, te.content, te.pos );

			GUI.TextField ( new Rect ( pixelselpos.x - textStyle.border.left - 2f , pixelselpos.y - textStyle.border.top, pixelpos.x, pixelpos.y ), wordtext);
			if(wordtext.Contains("\n"))
				lines++;
			te.MoveToStartOfNextWord ( );			
		}
		lines++;

		//Reposition selection
		Vector2 bkpixelselpos = textStyle.GetCursorPixelPosition ( te.position, te.content, backupSelPos );
		te.MoveCursorToPosition ( bkpixelselpos );

		//Remake selection
		Vector2 bkpixelpos = textStyle.GetCursorPixelPosition ( te.position, te.content, backupPos );
		te.SelectToPosition ( bkpixelpos );		
	}	

	Color CheckSyntax (string syntax )
	{
		string newSyntax = syntax.TrimEnd ( ' ' );		
		foreach ( string st in cNamespaces )
		{			
			if ( newSyntax == st  !isCommenting)
				return Color.cyan;
		}

		if ( newSyntax.StartsWith ( "//" ) || newSyntax.StartsWith ( "*/" ) )
		{
			isCommenting = true;
			if ( newSyntax.StartsWith ( "*/" ) )
				commentAffectsNextLine = true;
			return Color.green;
		}

		if ( newSyntax.StartsWith ( "\"" ) || newSyntax.StartsWith ( "\'" ) )
		{
			isString = true;
			return Color.magenta;
		}

		if ( newSyntax.StartsWith ( "/*" ) )
		{
			commentAffectsNextLine = false;
			return Color.green;
		}

		if ( isString  newSyntax != "\n" !isCommenting )
			return Color.magenta;

		if(isCommenting  newSyntax != "\n")
			return Color.green;

		if(isCommenting  (newSyntax == "\n"  commentAffectsNextLine ))
			return Color.green;

		if ( newSyntax == "\n" )
		{
			isCommenting = false;
			isString = false;
		}

		return Color.white;
	}

	void CheckKeys (TextEditor te, Event ev )
	{
		if ( GUIUtility.keyboardControl==te.controlID   ev.Equals ( Event.KeyboardEvent ( "tab" ) ) )
		{
			Debug.Log ( "tab pressed" );
			GUI.FocusControl ( "TextArea" );
			if ( s.Length > te.pos )
			{
				s = s.Insert ( te.pos, "\t" );
				te.pos++;
				te.selectPos = te.pos;
			}
			ev.Use ( );
			GUI.FocusControl ( "TextArea" );
		}

		if ( ( Event.current.type == EventType.KeyUp )  ( Event.current.keyCode == KeyCode.Space )  ( GUI.GetNameOfFocusedControl ( ) == "MyTextArea" ) )
		{
			Debug.Log ( "space pressed" );
		}

		if ( ( Event.current.type == EventType.KeyUp )  ( Event.current.keyCode == KeyCode.Return )  ( GUI.GetNameOfFocusedControl ( ) == "MyTextArea" ) )
		{

			Debug.Log ( "Enter pressed" );
			if ( s.Length > te.pos )
			{
				s = s.Insert ( te.pos, "\t" );
				te.pos++;
				te.selectPos = te.pos;
			}
			ev.Use ( );
		}
	}
}
3 Likes

Sounds interesting. Can you debug with it? Also what other advantages does it have over mono or vs?

What ever the community wants to add to it to make it better, it’s open source. And I do plan to add debugging and error reporting. So yea if any one wants to add to this system and post what they added they can. As for advantages to vs or mono - the only current one is this has no load time. It’s run inside the editor so you never have to leave Unity to script and when you save it updates the editor.

most interesting project right now :slight_smile:

I code/script in notepad++ at the moment, but I never got it to work with the unity debugging feature. :confused:
if you’d manage to enable debugging with yours I would consider leaving my beloved notepad++

I’d love to have “duplicate lines” with strg+D and “Line swapping” via strg+T (both Notepad++ features) in your unity script editor :slight_smile:

awesome idea, btw. thrilled to see more.

You should get this on a GitHub repository of some kind, and give the community the link. Personally, I’d be interested in building fully-featured refactoring tools into this, as I don’t think anything short of a VS install with ReSharper has in-depth refactoring capability.

I love the idea of not having to switch between programs, awesome stuff!

Added two menu items - File and Edit
File contains options for creating, loading and saving files.
Edit contains options for cutting, copying and pasting text.
Also you can now open multiple scripts at one time. They are stored in tabs in an upper menu bar.

Uploading soon.

Ok, on my first post I updated the script at the bottom to the current version. Enjoy :smile:

I typed in hello world and got dlrow olleh


Maybe I didn’t set the script right, not really sure. Really would appreciate some help. Oh and before I go I really appreciate your hard work on this quite amazing asset. Thank you.

Carl

Fixed Carl, just recopy the script and all should be good ^^.

Hey man, I hate to be a bearer of bad news but Im running Mac OS X and I can not load or save scripts, which kind of throws a wrench in me using this plug-in.

You can’t load or save… I have no mac so I’m not aware of this error… What does it say / do?

When I try to load a script, it goes to Finder(mac way of searching through files) like it should to find and open a script, but all the scripts are grey like they are not a file that this program could use. Save as does the same thing it try’s to open a file rather then save a new file. Clicking save gives me

ArgumentException: Path is empty
System.IO.FileStream…ctor (System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) (at /Applications/buildAgent/work/84669f285f6a667f/mcs/class/corlib/System.IO/FileStream.cs:209)
System.IO.FileStream…ctor (System.String path, FileMode mode)
(wrapper remoting-invoke-with-check) System.IO.FileStream:.ctor (string,System.IO.FileMode)
ScriptEditor.CheckFileOptions (UnityEngine.TextEditor te) (at Assets/-mine/scripts/ScriptEditor.cs:789)

when I double click it it sends me to line 789 fs = new FileStream ( this.path, FileMode.Open );

I think the FileStream class is a windows-only thing, just guessing

You probably need to do some #if #endif statements for the different platforms, as each has it’s own way of loading and saving files etc

this is handy to know. i wish i had a mac to test this on lol. If any one can help with this mac issue it would be greatly appreciated. I’ll try and figure it out my self as well.

Any thing new?

slkjdfv any progress?

No it isn’t. The exception is stating that this.path is null.

you still working on this?

This is opensource, this project also relies on others to contribute and add to this system. I am working on this project but only in my spare time. I have other projects that are more important at the moment. If anyone wants to contribute something it would be greatly appreciated. :slight_smile: So the answer is yes I am still working on it but it’s on the bottom of my list for now sorry.