Introduction
During my quest for that perfect text editor, I found many potential possibilities, but none that quite hit the mark. Some text editors would do this, but not that,
and so on... Thus, Peter was conceived, in hope to eliminate some of the downfalls found in other text editors.
Some of the main items that I wanted in Peter are:
- Code editor with highlighting
- Superior docking control
- Plug-in interface
- Code analysis
- And many others...
Code Explained
Editor
When thinking about the actual code editor, I thought of many different solutions:
- Create my own - which would probably be too long and difficult for my project scope.
- Use the
RichTextBox
- this was my first attempt, but the RichTextBox
could just not handle what I needed it to. - Use ScintillaNET (located here) - this was a very good possibility, but I noticed that the project
had not been updated since 2004, and I wanted a more recent solution.
So, I decided to use the ICSharpCode.TextEditor
found in the #Develop project located here.
Peter uses the ICSharpCode.TextEditor
from version 2.0, because version 3.0 has some errors that I did not have time to track down.
To use the editor in your project, download the source code for #Develop (I know it's a big download for some of us =P), extract the files to a folder, then go
to src\Libraries\ICSharpCode.TextEditor, and open the solution file found there. Build the solution, then go back to the folder you extracted the files to,
and you will find the DLL in the bin folder (same directory as the src folder). You can add this DLL via the Toolbox in Design mode, or add it as
a Reference in the Solution Explorer.
When using the ICSharpCode.TextEditor
, a good thing to remember is that the TextEditor
is a house for several different underlying controls.
So, if you are looking for a particular property/method, chances are pretty good that you will have to do a little digging before you find it. Some of the important
properties/methods are explained below:
using ICSharpCode.TextEditor;
namespace Editor101
{
public class MyEditor
{
private TextEditorControl m_Editor;
#region -= Constructor =-
public MyEditor(string pathToFile) : UserControl
{
this.m_Editor = new TextEditorControl();
this.m_Editor.Dock = System.Windows.Forms.DockStyle.Fill;
this.Controls.Add(this.m_Editor);
this.SetupEditor();
this.m_Editor.LoadFile(pathToFile, true, true);
this.m_Editor.ActiveTextAreaControl.TextArea.AllowDrop = true;
this.m_Editor.ActiveTextAreaControl.TextArea.DragEnter +=
new System.Windows.Forms.DragEventHandler(TextArea_DragEnter);
this.m_Editor.ActiveTextAreaControl.TextArea.DragDrop +=
new System.Windows.Forms.DragEventHandler(TextArea_DragDrop);
this.m_Editor.ActiveTextAreaControl.Caret.PositionChanged +=
new EventHandler(Caret_Change);
this.m_Editor.ActiveTextAreaControl.Caret.CaretModeChanged +=
new EventHandler(Caret_CaretModeChanged);
this.m_Editor.Document.DocumentChanged +=
new DocumentEventHandler(Document_DocumentChanged);
this.m_Editor.Document.UndoStack.ActionRedone +=
new EventHandler(UndoStack_ActionRedone);
this.m_Editor.Document.UndoStack.ActionUndone +=
new EventHandler(UndoStack_ActionRedone);
}
#endregion
#region -= Set up the Editor =-
private void SetupEditor()
{
string path = SCHEME_FOLDER;
HighlightingManager.Manager.AddSyntaxModeFileProvider(
new FileSyntaxModeProvider(path));
this.m_Editor.Document.HighlightingStrategy =
HighlightingManager.Manager.FindHighlighter("HTML");
this.m_Editor.ShowEOLMarkers = false;
this.m_Editor.ShowInvalidLines = false;
this.m_Editor.ShowSpaces = false;
this.m_Editor.ShowTabs = true;
this.m_Editor.ShowMatchingBracket = true;
switch (BracketMatchingStyle.ToLower())
{
case "before":
this.m_Editor.BracketMatchingStyle =
BracketMatchingStyle.Before;
break;
case "after":
this.m_Editor.BracketMatchingStyle =
BracketMatchingStyle.After;
break;
}
this.m_Editor.ShowLineNumbers = true;
this.m_Editor.ShowHRuler = false;
this.m_Editor.ShowVRuler = true;
this.m_Editor.EnableFolding = false;
this.m_Editor.Font = this.Font;
this.m_Editor.ConvertTabsToSpaces = false;
this.m_Editor.TabIndent = 4;
this.m_Editor.VRulerRow = 80;
this.m_Editor.AllowCaretBeyondEOL = false;
this.m_Editor.TextEditorProperties.AutoInsertCurlyBracket = false;
this.m_Editor.LineViewerStyle = (HighlightCurrentLine) ?
LineViewerStyle.FullRow : LineViewerStyle.None;
this.m_Editor.UseAntiAliasFont = true;
switch (IndentStyle.ToLower())
{
case "auto":
this.m_Editor.IndentStyle = IndentStyle.Auto;
break;
case "none":
this.m_Editor.IndentStyle = IndentStyle.None;
break;
case "smart":
this.m_Editor.IndentStyle = IndentStyle.Smart;
break;
}
}
#endregion
#region -= Misc =-
private void UndoStack_ActionRedone(object sender, EventArgs e)
{
this.m_Editor.ActiveTextAreaControl.TextArea.Invalidate();
}
public void Save(string filePath)
{
this.m_Editor.SaveFile(filePath);
}
public void Print()
{
PrintPreviewDialog dlg = new PrintPreviewDialog();
dlg.Document = this.m_Editor.PrintDocument;
dlg.ShowDialog();
}
public void Duplicate()
{
if (this.m_Editor.ActiveTextAreaControl.SelectionManager.HasSomethingSelected)
{
string selection = this.m_Editor.ActiveTextAreaControl.
SelectionManager.SelectedText;
int pos = this.m_Editor.ActiveTextAreaControl.
SelectionManager.SelectionCollection[0].EndOffset;
this.m_Editor.Document.Insert(pos, selection);
this.m_Editor.ActiveTextAreaControl.TextArea.Invalidate();
}
}
public void ScrollTo(int offset)
{
if (offset > this.m_Editor.Document.TextLength)
{
return;
}
int line = this.m_Editor.Document.GetLineNumberForOffset(offset);
this.m_Editor.ActiveTextAreaControl.Caret.Position =
this.m_Editor.Document.OffsetToPosition(offset);
this.m_Editor.ActiveTextAreaControl.ScrollTo(line);
}
#endregion
}
}
For more information about how to use the TextEditor
, you can always go to the #Develop forums.
Docking
To accomplish Docking, I first created a tab control that allowed tabbed groups. My tab control was not as sophisticated as I wanted, but a good tab control was found
in another article. As I was doing some research for the control, I stumbled across Weifen Luo's DockPanel Suite. This dock suite was exactly what I was looking for.
The project can be found here.
To use the dock suite, you need to first set your main form as a MDI Parent. Next, in the Toolbox for the form designer, right click, and select Choose Items...
In the .NET Framework Components tab, click Browse, and find the dock suite DLL that you downloaded. Select it and press OK. Now, back in the Toolbox, you should find
a DockPanel
component, add this to your form (it has to be a control of the form, not a panel), and the rest is in the code.
Once the DockPanel
has been added to your form, you need to create content (tabs) for it, called DockContent
. To do this, create a new class,
call it whatever you want, and then extend it with the DockContent
object.
using WeifenLuo.WinFormsUI.Docking;
namespace Docking101
{
public class MyDockContentTab : DockContent
{
}
}
Once you extend the DockContent
object, your class will be converted to a form, and you can add/remove items via code, or in Design mode.
Some of the important properties of the DockContent
are explained below:
ToolTipText
- This is the text that will be displayed when you hover your mouse over the tab.TabText
- This is the text that will be displayed on the tab.TabPageContextMenu/TabPageContextMenuStrip
- This is the ContextMenu
that will be displayed when someone right clicks on the tab
of the DockContent
.DockAreas
- This determines where the tab can be docked.HideOnClose
- When the user closes the tab via the "X" button, this will make the DockPanel
either Hide or Close (Dispose)
the DockContent
or Form
.
To add the DockContent
, you can follow the code below:
MyDockContentTab tabToAdd = new MyDockContentTab();
tabToAdd.Show(this.DockPanel);
DockState state = DockState.DockLeftAutoHide;
tabToAdd.Show(this.DockPanel, state);
tabToAdd.Show(this.DockPanel, new Rectangle(left, top, width, height));
DockPane pane = anotherTab.Pane;
tabToAdd.Show(pane, anotherTab);
This short intro to the DockPanel
should get you up and running.
Plug-in Interface
To use plug-ins in your application, you basically need to think of every thing someone would want to do via a plug-in, and put it into an interface. This can be
hard at times, so I would like to say that the Plug-in Interface for Peter is still a work in progress.
There are three interfaces for plug-in support in Peter: IPeterPlugin
, IPeterPluginHost
, and IPeterPluginTab
. Plug-ins are
loaded out of the Plugins folder when Peter is starting up, so no Plug-in manager is needed.
private void LoadPlugins()
{
string[] files = Directory.GetFiles(PLUGIN_FOLDER, "*.dll");
foreach (string file in files)
{
this.LoadPlugin(file);
}
}
public bool LoadPlugin(string pluginPath)
{
Assembly asm;
if (!File.Exists(pluginPath))
{
return false;
}
asm = Assembly.LoadFile(pluginPath);
if (asm != null)
{
foreach (Type type in asm.GetTypes())
{
if (type.IsAbstract)
continue;
object[] attrs = type.GetCustomAttributes(typeof(PeterPluginAttribute), true);
if (attrs.Length > 0)
{
IPeterPlugin plugin = Activator.CreateInstance(type) as IPeterPlugin;
plugin.Host = this;
if (plugin.HasMenu)
{
this.mnuPlugins.DropDownItems.Add(plugin.GetMenu());
}
if (plugin.HasTabMenu)
{
this.ctxTab.Items.Add(new ToolStripSeparator());
foreach (ToolStripMenuItem tsmi in plugin.GetTabMenu())
{
this.ctxTab.Items.Add(tsmi);
}
}
if (plugin.HasContextMenu)
{
this.ctxEditor.Items.Add(new ToolStripSeparator());
foreach (ToolStripMenuItem tsmi in plugin.GetContextMenu())
{
this.ctxEditor.Items.Add(tsmi);
}
}
this.m_Plugins.Add(plugin);
plugin.Start();
}
}
return true;
}
else
{
return false;
}
}
We first need to start off with the PeterPluginType
. This can be DockWindow
or UserDefined
. If PeterPluginType
is a DockWindow
, it will be displayed as a tab in Peter; otherwise, the user can control how the plug-in will be displayed, if needed at all. To specify
the PeterPluginType
, you need to add the PeterPlugin
attribute to your class (see below) and specify which PeterPluginType
you are using.
If this is not done, Peter will not recognize your plug-in.
The first interface is the IPeterPlugin
. This is the interface that your actual plug-in will need to use in order to work with Peter. For a plug-in example,
we will use the InternetBrowser
plug-in (located at Sourceforge).
using PeterInterface;
namespace InternetBrowser
{
[PeterPlugin(PeterPluginType.DockWindow)]
public class InternetBrowser : IPeterPlugin
{
private IPeterPluginHost m_Host;
private IDockContent m_ActiveTab;
public InternetBrowser()
{
this.m_ActiveTab = null;
}
#region -= IPeterPlugin Members =-
...
#endregion
}
}
Here are the IPeterPlugin
properties/methods:
public interface IPeterPlugin
{
void Start();
void Close();
string Name { get; }
bool AbleToLoadFiles { get; }
bool LoadFile(string filePath);
bool HasMenu { get; }
bool HasTabMenu { get; }
bool HasContextMenu { get; }
string Author { get; }
string Version { get; }
Image PluginImage { get; }
ToolStripMenuItem GetMenu();
ToolStripMenuItem[] GetTabMenu();
ToolStripMenuItem[] GetContextMenu();
PeterPluginType Type { get; }
IPeterPluginHost Host { get; set; }
void ActiveContentChanged(IDockContent tab);
bool CheckContentString(string contentString);
IDockContent GetContent(string contentString);
Control OptionPanel { get; }
void ApplyOptions();
}
The IPeterPluginHost
is the interface that is used for your plug-in to talk with Peter. I kept the interface between the plug-in and Peter very
short and sweet so as to minimize on potential threats. The host is set in the IPeterPlugin
interface above. Its properties/methods are shown below:
public interface IPeterPluginHost
{
string EditorType { get; }
string ApplicationExeStartPath { get; }
void NewDocument();
void Trace(string text);
void SaveAs(IPeterPluginTab tab);
void CreateEditor(string path, string tabName);
void CreateEditor(string path, string tabName, Icon image);
void CreateEditor(string path, string tabName, Icon image, IDockContent addToContent);
Icon GetFileIcon(string path, bool linkOverlay);
void AddDockContent(DockContent content);
void AddDockContent(DockContent content, DockState state);
void AddDockContent(DockContent content, Rectangle floatingRec);
}
The last interface is the IPeterPluginTab
. This interface is needed if you plan on implementing a tab in Peter. If you open the solution for
the InternetBrowser
plug-in, you will see that there is a InternetBrowser
class and a ctrlInternetBrowser
DockContent
.
The ctrlInternetBrowser
implements the IPeterPluginTab
interface because it will be the tab that hosts the web browser. You can think
of the IPeterPlugin
as behind the scene, and IPeterPluginTab
as what everyone sees. This way, we can use the menu items like Find, Copy, Save...
in your plug-in's tab. Your tab will need to implement the properties/methods below (they are all pretty self-explanatory):
public interface IPeterPluginTab
{
void Save();
void SaveAs(string filePath);
void Cut();
void Copy();
void Paste();
void Undo();
void Redo();
void Delete();
void Print();
void SelectAll();
void Duplicate();
bool CloseTab();
IPeterPluginHost Host { get; set; }
string FileName { get; }
string Selection { get; }
bool AbleToUndo { get; }
bool AbleToRedo { get; }
bool AbleToPaste { get; }
bool AbleToCut { get; }
bool AbleToCopy { get; }
bool AbleToSelectAll { get; }
bool AbleToSave { get; }
bool AbleToDelete { get; }
bool NeedsSaving { get; }
string TabText { get; set; }
void MarkAll(Regex reg);
bool FindNext(Regex reg, bool searchUp);
void ReplaceNext(Regex reg, string replaceWith, bool searchUp);
void ReplaceAll(Regex reg, string replaceWith);
void SelectWord(int line, int offset, int wordLeng);
}
For more info on plug-ins, open the InternetBrowser plug-in and take a look at its code. Suggestions for improving the plug-in interface are welcome.
Code Analysis
One of the last major feats of Peter was the code analysis. This became very tricky, and will never be 100 percent complete. Many options were explored, but I finally decided to use
Coco/R. Coco/R is a compiler generator that will create a scanner and parser for you. The only problem is that you have to implement
the parser yourself. To create a parser for a certain style of code, you need to create an ATG (attributed grammar - don't quote me on this) file. This file will tell Coco
how it needs to create the parser's code. Once you have your ATG file, you give it to Coco/R, and out will pop a scanner and parser written in the version of code you downloaded
Coco/R in (C#, C++, Java, etc.). More info on this can be found at the Coco/R website. The code analyzer is actually an internal plug-in. I made it internal so I could have more
control over it. So, when the active document is changed (see Plug-in Interface), we send the parser plug-in the document we want to parse. It will then determine the file extension
and parse the code accordingly. So far, I have implemented XML, C#, CSS, Java, and .C/.H file parsers.
An important item to note is that when the parser that Coco/R creates parses the file, it does nothing to save the information that was parsed. So, you have to go in
and create a way to save the parsed info. I did this by just adding some ArrayList
s that will hold the info I need, like an ArrayList
to hold the methods.
So, how the whole thing works is:
- Download Coco/R (I downloaded Coco/R for C#, so it will give me C# parsers)
- Create an ATG file for the code style of your choice
/* A small sample of the ATG File for C#... */
CS2
=
{IF (IsExternAliasDirective()) ExternAliasDirective}
{UsingDirective}
{IF (IsGlobalAttrTarget()) GlobalAttributes}
{NamespaceMemberDeclaration}
.
ExternAliasDirective
=
"extern" ident (.
if (t.val != "alias") {
Error("alias expected");
}
.)
ident ";"
.
UsingDirective
=
"using" [ IF (IsAssignment()) ident "=" ]
TypeName ";"
Give the ATG file to Coco/R, it will create two C# files -- scanner.cs and parser.cs
coco CSharp.ATG
Add these two files to your projectEdit the files to save the needed information
void UsingDirective()
{
Expect(78);
if (IsAssignment())
{
Expect(1);
Expect(85);
}
TokenMatch tm = new TokenMatch();
tm.Position = la.pos;
this.m_Current = la.val;
TypeName();
tm.Value = this.m_Current;
this.m_CodeInfo.Usings.Add(tm);
Expect(114);
}
Create a scanner and a parser
Peter.CSParser.Scanner scanner = new Peter.CSParser.Scanner(pathToFile);
Peter.CSParser.Parser parser = new Peter.CSParser.Parser(scanner);
Parse the file
parser.Parse();
Add the collected information to a tree
TreeNode nUsing = new TreeNode("Usings");
foreach (TokenMatch tm in parser.CodeInfo.Usings)
{
TreeNode n = new TreeNode(tm.Value);
n.Tag = tm.Position;
nUsing.Nodes.Add(n);
}
if (nUsing.Nodes.Count > 0)
{
myTree.Nodes.Add(nUsing);
}
Conclusion
Well, that's all I have for Peter right now. There are still some other features that I did not explain, maybe I will at another date, but I did not think they
were too important. These features include: the file explorer, project support, the Find dialog, command prompt, file difference, and folding strategies. If you need
help with these, please let me know, and I will publish some info on it.
History
- 27 May 2008: Article created.
- 8 Feb 2012: Download links updated.