SharpDevelop浅析_3_文档编辑器、语法高亮显示
1、Demo界面及功能解释
启动后,打开文档(默认支持.cs, .js, .java, .aspx等类型文件的语法高亮显示,详见ICSharpCode.TextEditor\Resources\SyntaxModes.xml)、切换语言界面如下:

切换为中文语言环境后的界面如下:

功能说明:可以实时改变语言环境;提供对常用编程语言的编辑:支持语法高亮显示、括号匹配、设置书签;尚未提供查找/替换、代码折叠、代码提示/自动完成等功能。
2、SharpDevelop的Internationalization的使用
多语言的实现就是在显示时根据键获取相应语言环境下的键值(Dictionary<key,value>),因此在编写程序时应该引用键,而非直接书写要显示的字符。一般地,应用程序的显示包括菜单、状态栏、提示字符等,因此Demo中的主菜单在配置文件(参见Basic.addin)中使用label = "${res:Menu.File.Open}",退出应用程序的提示字符使用string s = StringParser.Parse("${res:Info.Exit}");。ICSharpCode.Core.dll支持语言环境的实时修改(修改后语言设置不需重启应用程序),因此要在应用程序订阅语言环境改变的事件,相关代码如下:

语言环境的修改及事件响应
1
//a, 设置语言
2
//引自SharpPad项目的Dialogs\SelectCulturePanel.cs
3
public override bool ReceiveDialogMessage(DialogMessage message)
4
{
5
if (message == DialogMessage.OK) {
6
if (SelectedCulture != null) {
7
PropertyService.Set("CoreProperties.UILanguage", SelectedCulture);
8
}
9
}
10
return true;
11
}
12
//返回当前窗体用户选择的语言设置(从显示国家国旗的ListView控件)
13
string SelectedCulture {
14
get {
15
if (listView.SelectedItems.Count > 0) {
16
return listView.SelectedItems[0].SubItems[1].Text;
17
}
18
return null;
19
}
20
}
21
//b, 刷新当前应用程序
22
//引自SharpPad项目的SharpPad.cs
23
void IniFrm()
24
{
25
//
26
_menuStrip = new MenuStrip();
27
MenuService.AddItemsToMenu(_menuStrip.Items, this, "/michael/myMenus");
28
this.Controls.Add(_menuStrip);
29
30
PropertyService.PropertyChanged += new PropertyChangedEventHandler(PropertyService_PropertyChanged);
31
ResourceService.LanguageChanged += delegate {
32
//更新菜单项的Text显示
33
foreach (ToolStripItem item in _menuStrip.Items)
34
{
35
if (item is IStatusUpdate)
36
{
37
((IStatusUpdate)item).UpdateText();
38
}
39
}
40
};
41
}
42
//引自ICSharpCode.Core的src\AddinTree\Addin\DefaultDoozers\MenuItem\Gui\IStatusUpdate.Core
43
using System;
44
namespace ICSharpCode.Core
45
{
46
public interface IStatusUpdate
47
{
48
void UpdateStatus();
49
void UpdateText();
50
}
51
}
另外,可以看到Demo中的选项命令窗口也采用了插件模式来构造窗体(实现细节就不多谈了),容器窗体是TreeViewOption,插件窗体如:SelectCulturePanel, SelectStylePanel, DemoNothing,真是“扩展--无处不在”呀。SharpDevelop源码中的这些窗体的成员控件是通过.xfrm配置文件配置,窗体继承自XmlUserControl来根据配置文件生成控件,有一定的灵活性。
新的语言包资源文件放在\data\resources\目录下(如StringResources.cn-gb.resources),注意应用程序默认语言选项以及在未找到相关语言资源时都是引用Entry中的myRes.resx资源文件;语言声明文件是\data\resources\languages\LanguageDefinition.xml,其格式如下:

LanguageDefinition.xml
1
<Languages>
2
<Languages name="Chinese (GB)" code="cn-gb" page="" icon="chinalg.png" />
3
<Languages name="German" code="de" page="" icon="germany.png" />
4
<Languages name="English" code="en" page="" icon="uk.png" />
5
</Languages>
SharpPad中动态显示可选语言项的分析类是LanguageService.cs和Language.cs,此处就不多解释了。
3、SharpDevelop的Internationalization的实现分析
SharpDevelop的多语言支持的键值对是通过本地资源文件存储的,当Demo中更改语言环境设置时,引发ICSharpCode.Core.dll中的事件及方法顺序如下:
PropertyService类的属性更新引发PropertyChanged事件 -> ResourceService响应接收到的事件并重新加载保存在内存中的资源键值对,然后引发LanguageChanged事件(由使用端接收并作相关处理,如Demo中的SharpPad.cs中的事件订阅/处理)属性更新的相关代码如下:

属性更改
1
//PropertyService类实际是提供了对Properties类的包装,因此直接看Properties类中的代码:
2
//引自ICSharpCode.Core\src\Services\PropertService\Properties.cs
3
public void Set<T>(string property, T value)
4
{
5
T oldValue = default(T);
6
if (!properties.ContainsKey(property)) {
7
properties.Add(property, value);
8
} else {
9
oldValue = Get<T>(property, value);
10
properties[property] = value;
11
}
12
OnPropertyChanged(new PropertyChangedEventArgs(this, property, oldValue, value));
13
}
14
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
15
{
16
if (PropertyChanged != null) {
17
PropertyChanged(this, e);
18
}
19
}
20
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
21
public event PropertyChangedEventHandler PropertyChanged;
资源服务类的事件响应代码如下:

ResourceService类的事件响应
1
//引自ICSharpCode.Core\src\Services\ResourceService\ResourceService.cs
2
//首先在类的初始化中订阅属性更改事件:
3
PropertyService.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChange);
4
//类级别变量:
5
static Hashtable localStrings = null;
6
static Hashtable localIcons = null;
7
public static event EventHandler LanguageChanged;
8
//事件响应:
9
static void OnPropertyChange(object sender, PropertyChangedEventArgs e)
10
{
11
if (e.Key == uiLanguageProperty && e.NewValue != e.OldValue) {
12
LoadLanguageResources((string)e.NewValue);
13
if (LanguageChanged != null)
14
LanguageChanged(null, e);
15
}
16
}
17
static void LoadLanguageResources(string language)
18
{
19
try
20
{
21
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language);
22
}
23
catch (Exception)
24
{
25
try
26
{
27
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language.Split('-')[0]);
28
}
29
catch (Exception) { }
30
}
31
32
localStrings = Load(stringResources, language);
33
if (localStrings == null && language.IndexOf('-') > 0)
34
{
35
localStrings = Load(stringResources, language.Split('-')[0]);
36
}
37
38
localIcons = Load(imageResources, language);
39
if (localIcons == null && language.IndexOf('-') > 0)
40
{
41
localIcons = Load(imageResources, language.Split('-')[0]);
42
}
43
44
localStringsResMgrs.Clear();
45
localIconsResMgrs.Clear();
46
currentLanguage = language;
47
foreach (ResourceAssembly ra in resourceAssemblies)
48
{
49
ra.Load();
50
}
51
}
52
static Hashtable Load(string name, string language)
53
{
54
return Load(resourceDirectory + Path.DirectorySeparatorChar + name + "." + language + ".resources");
55
}
56
static Hashtable Load(string fileName)
57
{
58
if (File.Exists(fileName)) {
59
Hashtable resources = new Hashtable();
60
ResourceReader rr = new ResourceReader(fileName);
61
foreach (DictionaryEntry entry in rr) {
62
resources.Add(entry.Key, entry.Value);
63
}
64
rr.Close();
65
return resources;
66
}
67
return null;
68
}
4、SharpDevelop的文档管理的基本概念
代码编写工具的核心是代码编辑窗口(如打开一个.cs文件的窗口),如何在应用程序中存储该窗口内的字符内容?有些文件可能有上千行,统计下来可能有过万个字符,如果使用string对象存储字符内容,显然是不能满足性能要求,而且要实现语法高亮显示的话,还需要区分存储TextWord和相应颜色……
先看一下字符管理的基本要求:

ITextBufferStrategy接口
1
//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\ITextBufferStrategy.cs
2
namespace ICSharpCode.TextEditor.Document
3
{
4
/// <summary>
5
/// Interface to describe a sequence of characters that can be edited.
6
/// </summary>
7
public interface ITextBufferStrategy
8
{
9
/// <value>
10
/// The current length of the sequence of characters that can be edited.
11
/// </value>
12
int Length {
13
get;
14
}
15
16
/// <summary>
17
/// Inserts a string of characters into the sequence.
18
/// </summary>
19
/// <param name="offset">
20
/// offset where to insert the string.
21
/// </param>
22
/// <param name="text">
23
/// text to be inserted.
24
/// </param>
25
void Insert(int offset, string text);
26
27
/// <summary>
28
/// Removes some portion of the sequence.
29
/// </summary>
30
/// <param name="offset">
31
/// offset of the remove.
32
/// </param>
33
/// <param name="length">
34
/// number of characters to remove.
35
/// </param>
36
void Remove(int offset, int length);
37
38
/// <summary>
39
/// Replace some portion of the sequence.
40
/// </summary>
41
/// <param name="offset">
42
/// offset.
43
/// </param>
44
/// <param name="length">
45
/// number of characters to replace.
46
/// </param>
47
/// <param name="text">
48
/// text to be replaced with.
49
/// </param>
50
void Replace(int offset, int length, string text);
51
52
/// <summary>
53
/// Fetches a string of characters contained in the sequence.
54
/// </summary>
55
/// <param name="offset">
56
/// Offset into the sequence to fetch
57
/// </param>
58
/// <param name="length">
59
/// number of characters to copy.
60
/// </param>
61
string GetText(int offset, int length);
62
63
/// <summary>
64
/// Returns a specific char of the sequence.
65
/// </summary>
66
/// <param name="offset">
67
/// Offset of the char to get.
68
/// </param>
69
char GetCharAt(int offset);
70
71
/// <summary>
72
/// This method sets the stored content.
73
/// </summary>
74
/// <param name="text">
75
/// The string that represents the character sequence.
76
/// </param>
77
void SetContent(string text);
78
}
79
}
SharpDevelop采用的策略是使用带Gap的字符,Gap的长度小于预定规格时,按约定增加一定长度(有些类似于SqlServer的表空间管理?),插入/修改/删除字符时只是修改Gap的长度和位置和移动部分字符,核心函数如下:

GapTextBufferStrategy类
1
//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\GapTextBufferStrategy.cs
2
//类级别变量:
3
char[] buffer = new char[0];
4
int gapBeginOffset = 0;
5
int gapEndOffset = 0;
6
/// <summary>
7
/// 小于此长度时增长Buffer/Gap
8
/// </summary>
9
int minGapLength = 32;
10
/// <summary>
11
/// 每次需要增长时,增长此长度的Gap
12
/// </summary>
13
int maxGapLength = 256;
14
//重要函数:
15
public void SetContent(string text)
16
{
17
if (text == null) {
18
text = String.Empty;
19
}
20
buffer = text.ToCharArray();
21
gapBeginOffset = gapEndOffset = 0;
22
}
23
public void Insert(int offset, string text)
24
{
25
Replace(offset, 0, text);
26
}
27
public void Remove(int offset, int length)
28
{
29
Replace(offset, length, String.Empty);
30
}
31
public void Replace(int offset, int length, string text)
32
{
33
if (text == null) {
34
text = String.Empty;
35
}
36
37
// Math.Max is used so that if we need to resize the array
38
// the new array has enough space for all old chars
39
PlaceGap(offset + length, Math.Max(text.Length - length, 0));
40
text.CopyTo(0, buffer, offset, text.Length);
41
gapBeginOffset += text.Length - length;
42
}
43
void PlaceGap(int offset, int length)
44
{
45
int deltaLength = GapLength - length;
46
// 如果Gap的长度足够大,则只是作移动相关字符的处理
47
if (minGapLength <= deltaLength && deltaLength <= maxGapLength) {
48
int delta = gapBeginOffset - offset;
49
// check if the gap is already in place
50
if (offset == gapBeginOffset) {
51
return;
52
} else if (offset < gapBeginOffset) {
53
int gapLength = gapEndOffset - gapBeginOffset;
54
Array.Copy(buffer, offset, buffer, offset + gapLength, delta);
55
} else { //offset > gapBeginOffset
56
Array.Copy(buffer, gapEndOffset, buffer, gapBeginOffset, -delta);
57
}
58
gapBeginOffset -= delta;
59
gapEndOffset -= delta;
60
return;
61
}
62
63
// 否则,需要新分析buffer的大小,并修改offset等位置数值
64
int oldLength = GapLength;
65
int newLength = maxGapLength + length;
66
int newGapEndOffset = offset + newLength;
67
char[] newBuffer = new char[buffer.Length + newLength - oldLength]; //新分配后将有maxGapLength长度的Gap
68
69
if (oldLength == 0) {
70
Array.Copy(buffer, 0, newBuffer, 0, offset);
71
Array.Copy(buffer, offset, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
72
} else if (offset < gapBeginOffset) {
73
int delta = gapBeginOffset - offset;
74
Array.Copy(buffer, 0, newBuffer, 0, offset);
75
Array.Copy(buffer, offset, newBuffer, newGapEndOffset, delta);
76
Array.Copy(buffer, gapEndOffset, newBuffer, newGapEndOffset + delta, buffer.Length - gapEndOffset);
77
} else {
78
int delta = offset - gapBeginOffset;
79
Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
80
Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, delta);
81
Array.Copy(buffer, gapEndOffset + delta, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
82
}
83
84
buffer = newBuffer;
85
gapBeginOffset = offset;
86
gapEndOffset = newGapEndOffset;
87
}
有了基本的数据容器,核心问题已经解决了,但是在绘制界面时直接使用上面的类,显然不够方便,于是就定义了LineSegment和TextWord类来分别存储行、单词,注意这两个类存储的只是int类型的offset和length信息,而不存储字符或字符串对象。注意TextWord类中有个HighlightColor类型的变量用以存储语法高亮显示信息。
有了上面的这些类,便可以组合起来补充些其它信息对外提供服务了,IDocument接口存在的目的即在于此,它封装了ITextEditorProperties(是否显示空格/Tab/Eol/HRuler等)、UndoStack、ITextBufferStrategy、IHighlightingStrategy、FoldingManager、BookmarkManager等属性对象。
5、SharpDevelop的SyntaxHighlighting配置文件的定义
是时候对语法高亮显示作一些分析了,此处不详述其实现,而重点分析其配置定义,从中亦可以猜出部分实现。
相关配置文件均保存在ICSharpCode.TextEditor项目\Resource\目录下
首先SyntaxMode.xml文件中显示了已定义的文件类型及其声明文件位置,其解析类参见\src\Document\HighlightingStrategy\SyntaxModes\FileSyntaxModeProvider.cs

SyntaxMode.xml
1
<SyntaxModes version="1.0">
2
<Mode file = "ASPX.xshd"
3
name = "ASP/XHTML"
4
extensions = ".asp;.aspx;.asax;.asmx"/>
5
6
<Mode file = "BAT-Mode.xshd"
7
name = "BAT"
8
extensions = ".bat"/>
9
10
<Mode file = "CPP-Mode.xshd"
11
name = "C++.NET"
12
extensions = ".c;.h;.cc;.C;.cpp;.hpp"/>
13
14
<Mode file = "CSharp-Mode.xshd"
15
name = "C#"
16
extensions = ".cs"/>
17
18
<Mode file = "XML-Mode.xshd"
19
name = "XML"
20
extensions = ".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd;.wxs;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets;.xaml;.xpt;.xft;.map;.wsdl;.disco"/>
21
</SyntaxModes>
取CSharp-Mode.xshd(注:xshd是Xml Syntax Highlighting Definition的缩写)为例,查看其定义:

CSharp-Mode.xshd
1
<?xml version="1.0"?>
2
<SyntaxDefinition name = "C#" extensions = ".cs">
3
<Properties>
4
<Property name="LineComment" value="//"/>
5
</Properties>
6
<Digits name = "Digits" bold = "false" italic = "false" color = "DarkBlue"/>
7
<RuleSets>
8
<RuleSet ignorecase="false">
9
<Delimiters>&<>~!%^*()-+=|\#/{}[]:;"' , .?</Delimiters>
10
<Span name = "PreprocessorDirectives" rule = "PreprocessorSet" bold="false" italic="false" color="Green" stopateol = "true">
11
<Begin>#</Begin>
12
</Span>
13
<Span name = "BlockComment" rule = "CommentMarkerSet" bold = "false" italic = "false" color = "Green" stopateol = "false">
14
<Begin>/*</Begin>
15
<End>*/</End>
16
</Span>
17
<KeyWords name = "Punctuation" bold = "false" italic = "false" color = "DarkGreen">
18
<Key word = "?" />
19
<Key word = "," />
20
<Key word = "." />
21
<Key word = ";" />
22
<Key word = "(" />
23
<Key word = ")" />
24
<Key word = "[" />
25
<Key word = "]" />
26
<Key word = "{" />
27
<Key word = "}" />
28
<Key word = "+" />
29
<Key word = "-" />
30
<Key word = "/" />
31
<Key word = "%" />
32
<Key word = "*" />
33
<Key word = "<" />
34
<Key word = ">" />
35
<Key word = "^" />
36
<Key word = "=" />
37
<Key word = "~" />
38
<Key word = "!" />
39
<Key word = "|" />
40
<Key word = "&" />
41
</KeyWords>
42
43
<KeyWords name = "AccessKeywords" bold="true" italic="false" color="Black">
44
<Key word = "this" />
45
<Key word = "base" />
46
</KeyWords>
47
48
<KeyWords name = "OperatorKeywords" bold="true" italic="false" color="DarkCyan">
49
<Key word = "as" />
50
<Key word = "is" />
51
<Key word = "new" />
52
<Key word = "sizeof" />
53
<Key word = "typeof" />
54
<Key word = "true" />
55
<Key word = "false" />
56
<Key word = "stackalloc" />
57
</KeyWords>
58
</RuleSet>
59
</RuleSets>
60
</SyntaxDefinition>
上面的文件公摘选了部分标签,其中
<Property>标签定义了指定名称属性的指定值
<Digits>标签定义了数值的字体显示样式
<Delimiters>定义了分隔单词的字符
<Span>定义了包含在此指定Begin/End中的字符的显示样式,注意没有<End>标签的一般设stopateol(stop at end-of-line)为true
<KeyWords>指定了其子集<Key>声明的单词的显示样式
6、SharpDevelop的TextEditor控件的实现概述
接下来的任务是要显示和支持用户输入了,直接使用TextBox或RichTextBox好像都不太现实,效率上也必定有不少损失,于是SharpDevelop的方式是直接继承自Control, Panel, UserControl 的方式来实现编辑控件(参见ICSharpCode.TextEditor项目\src\Gui\...)。
首先使用TextEditorControlBase(继承自UserControl)封装当前文件路径、Encoding、IDocument对象、ITextEditorProperties对象、快捷键列表(Dictionary<Keys, IEditAction>类型)的变量,提供LoadFile()、SaveFile()等重要方法。
TextEditorControl类继承自上面的类,并声明了Panel、Splitter、TextAreaControl、PrintDocument控件,其中显示文件的核心控件是TextAreaControl,该控件在此类中被声明了两个变量,默认只有一个primaryTextArea显示,支持切分为两个窗口的显示,当有两个窗口时,Splitter控件才有效;PrinDocument控件用以打印输出内容;该类的另一个重要功能是提供了UnDo()、Redo()方法。
TextAreaControl(继承自Panel)控件,封装了TextArea、VScrollBar、HScrollBar控件,其中TextArea负责文件数据显示,另外两个控件负责文件内容大于可见尺寸时的滚动条服务。
TextArea(继承自Control)封装了TextView, IconBarMargin, GutterMargin, FoldMargin, SelectionManager, Caret 等控件或类对象,其中前四个控件均继承自AbstractMargin,代表区域对象,各自负责字符区域(较大的文件内容绘制区)、图标区域(如Bookmark图标所在列)、Gutter区域(如行号)、折叠控制区域的绘制,SelectionManager用以控制选中项,Caret用以控制光标位置调整和显示。
下面是一些我在读代码的过程中有过的疑问及解答:
a, 加载文件时发生了什么?
答:加载文件时控件根据文件的后缀名选择了相应的高亮显示策略,然后读取文件的内容并生成相应的GapTextBufferStrategy, LineSegment, TextWord 等对象,并且对所有的TextWord对象的HighlightColor类型成员变量完成分析赋值(用以在Paint函数中显示),相关代码如下:

LoadFile()相关代码
1
//取自TextEditorControlBase.cs
2
public void LoadFile(string fileName, bool autoLoadHighlighting, bool autodetectEncoding)
3
{
4
BeginUpdate();
5
document.TextContent = String.Empty;
6
document.UndoStack.ClearAll();
7
document.BookmarkManager.Clear();
8
if (autoLoadHighlighting) {
9
//根据文件扩展名判断并赋值高亮显示的策略
10
document.HighlightingStrategy = HighlightingStrategyFactory.CreateHighlightingStrategyForFile(fileName);
11
}
12
13
if (autodetectEncoding) {
14
Encoding encoding = this.Encoding;
15
//赋值
16
Document.TextContent = Util.FileReader.ReadFileContent(fileName, ref encoding, this.TextEditorProperties.Encoding);
17
this.Encoding = encoding;
18
} else {
19
using (StreamReader reader = new StreamReader(fileName, this.Encoding)) {
20
Document.TextContent = reader.ReadToEnd();
21
}
22
}
23
24
this.FileName = fileName;
25
OptionsChanged();
26
Document.UpdateQueue.Clear();
27
EndUpdate();
28
29
Refresh();
30
}
31
//引自DefaultDocument.cs
32
ITextBufferStrategy textBufferStrategy = null;
33
ILineManager lineTrackingStrategy = null;
34
public string TextContent {
35
get {
36
return GetText(0, textBufferStrategy.Length);
37
}
38
set {
39
Debug.Assert(textBufferStrategy != null);
40
Debug.Assert(lineTrackingStrategy != null);
41
OnDocumentAboutToBeChanged(new DocumentEventArgs(this, 0, 0, value));
42
//赋值
43
textBufferStrategy.SetContent(value);
44
//赋值 && 分析并完成高亮分析的赋值
45
lineTrackingStrategy.SetContent(value);
46
47
OnDocumentChanged(new DocumentEventArgs(this, 0, 0, value));
48
OnTextContentChanged(EventArgs.Empty);
49
}
50
}
51
//引自DefaultLineManager.cs
52
public void SetContent(string text)
53
{
54
lineCollection.Clear();
55
if (text != null) {
56
textLength = text.Length;
57
// 生成LineSegment集合
58
CreateLines(text, 0, 0);
59
// 高亮分析
60
RunHighlighter();
61
}
62
}
b, 控件如何响应键盘事件?
答:对于方向键及快捷功能键通过预定义的实现IEditAction接口的类响应(执行功能,不影响字符内容);其它字母/数字键直接输入,同时执行更新Folding, Bookmark, Higlighting等属性信息。相关代码:

键盘事件响应
1
//功能键的定义(引自TextEditorControlBase.cs):
2
protected Dictionary<Keys, IEditAction> editactions = new Dictionary<Keys, IEditAction>();
3
protected TextEditorControlBase()
4
{
5
GenerateDefaultActions();
6
HighlightingManager.Manager.ReloadSyntaxHighlighting += new EventHandler(ReloadHighlighting);
7
}
8
void GenerateDefaultActions()
9
{
10
editactions[Keys.Left] = new CaretLeft();
11
editactions[Keys.Left | Keys.Shift] = new ShiftCaretLeft();
12
editactions[Keys.Left | Keys.Control] = new WordLeft();
13
editactions[Keys.Left | Keys.Control | Keys.Shift] = new ShiftWordLeft();
14
editactions[Keys.Right] = new CaretRight();
15
editactions[Keys.Right | Keys.Shift] = new ShiftCaretRight();
16
editactions[Keys.Right | Keys.Control] = new WordRight();
17
editactions[Keys.Right | Keys.Control | Keys.Shift] = new ShiftWordRight();
18
editactions[Keys.Up] = new CaretUp();
19
editactions[Keys.Up | Keys.Shift] = new ShiftCaretUp();
20
editactions[Keys.Up | Keys.Control] = new ScrollLineUp();
21
editactions[Keys.Down] = new CaretDown();
22
editactions[Keys.Down | Keys.Shift] = new ShiftCaretDown();
23
editactions[Keys.Down | Keys.Control] = new ScrollLineDown();
24
25
editactions[Keys.Insert] = new ToggleEditMode();
26
editactions[Keys.Insert | Keys.Control] = new Copy();
27
editactions[Keys.Insert | Keys.Shift] = new Paste();
28
editactions[Keys.Delete] = new Delete();
29
editactions[Keys.Delete | Keys.Shift] = new Cut();
30
editactions[Keys.Home] = new Home();
31
editactions[Keys.Home | Keys.Shift] = new ShiftHome();
32
editactions[Keys.Home | Keys.Control] = new MoveToStart();
33
editactions[Keys.Home | Keys.Control | Keys.Shift] = new ShiftMoveToStart();
34
editactions[Keys.End] = new End();
35
editactions[Keys.End | Keys.Shift] = new ShiftEnd();
36
editactions[Keys.End | Keys.Control] = new MoveToEnd();
37
editactions[Keys.End | Keys.Control | Keys.Shift] = new ShiftMoveToEnd();
38
editactions[Keys.PageUp] = new MovePageUp();
39
editactions[Keys.PageUp | Keys.Shift] = new ShiftMovePageUp();
40
editactions[Keys.PageDown] = new MovePageDown();
41
editactions[Keys.PageDown | Keys.Shift] = new ShiftMovePageDown();
42
43
editactions[Keys.Return] = new Return();
44
editactions[Keys.Tab] = new Tab();
45
editactions[Keys.Tab | Keys.Shift] = new ShiftTab();
46
editactions[Keys.Back] = new Backspace();
47
editactions[Keys.Back | Keys.Shift] = new Backspace();
48
49
editactions[Keys.X | Keys.Control] = new Cut();
50
editactions[Keys.C | Keys.Control] = new Copy();
51
editactions[Keys.V | Keys.Control] = new Paste();
52
53
editactions[Keys.A | Keys.Control] = new SelectWholeDocument();
54
editactions[Keys.Escape] = new ClearAllSelections();
55
56
editactions[Keys.Divide | Keys.Control] = new ToggleComment();
57
editactions[Keys.OemQuestion | Keys.Control] = new ToggleComment();
58
59
editactions[Keys.Back | Keys.Alt] = new Actions.Undo();
60
editactions[Keys.Z | Keys.Control] = new Actions.Undo();
61
editactions[Keys.Y | Keys.Control] = new Redo();
62
63
editactions[Keys.Delete | Keys.Control] = new DeleteWord();
64
editactions[Keys.Back | Keys.Control] = new WordBackspace();
65
editactions[Keys.D | Keys.Control] = new DeleteLine();
66
editactions[Keys.D | Keys.Shift | Keys.Control] = new DeleteToLineEnd();
67
68
editactions[Keys.B | Keys.Control] = new GotoMatchingBrace();
69
}
70
internal IEditAction GetEditAction(Keys keyData)
71
{
72
if (!editactions.ContainsKey(keyData)) {
73
return null;
74
}
75
return (IEditAction)editactions[keyData];
76
}
77
//功能键的响应(取自TextArea.cs):
78
protected override bool ProcessDialogKey(Keys keyData)
79
{
80
return ExecuteDialogKey(keyData) || base.ProcessDialogKey(keyData);
81
}
82
public bool ExecuteDialogKey(Keys keyData)
83
{
84
// try, if a dialog key processor was set to use this
85
if (DoProcessDialogKey != null && DoProcessDialogKey(keyData)) {
86
return true;
87
}
88
89
if (keyData == Keys.Back || keyData == Keys.Delete || keyData == Keys.Enter) {
90
if (TextEditorProperties.UseCustomLine == true) {
91
if (SelectionManager.HasSomethingSelected) {
92
if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
93
return true;
94
} else {
95
int curLineNr = Document.GetLineNumberForOffset(Caret.Offset);
96
if (Document.CustomLineManager.IsReadOnly(curLineNr, false) == true)
97
return true;
98
if ((Caret.Column == 0) && (curLineNr - 1 >= 0) && keyData == Keys.Back &&
99
Document.CustomLineManager.IsReadOnly(curLineNr - 1, false) == true)
100
return true;
101
if (keyData == Keys.Delete) {
102
LineSegment curLine = Document.GetLineSegment(curLineNr);
103
if (curLine.Offset + curLine.Length == Caret.Offset &&
104
Document.CustomLineManager.IsReadOnly(curLineNr + 1, false) == true) {
105
return true;
106
}
107
}
108
}
109
}
110
}
111
112
// if not (or the process was 'silent', use the standard edit actions
113
IEditAction action = motherTextEditorControl.GetEditAction(keyData);
114
AutoClearSelection = true;
115
if (action != null) {
116
motherTextEditorControl.BeginUpdate();
117
try {
118
lock (Document) {
119
// 执行相关的功能操作
120
action.Execute(this);
121
if (SelectionManager.HasSomethingSelected && AutoClearSelection /*&& caretchanged*/) {
122
if (Document.TextEditorProperties.DocumentSelectionMode == DocumentSelectionMode.Normal) {
123
SelectionManager.ClearSelection();
124
}
125
}
126
}
127
} finally {
128
motherTextEditorControl.EndUpdate();
129
Caret.UpdateCaretPosition();
130
}
131
return true;
132
}
133
return false;
134
}
135
136
//输入键的响应(取自TextArea.cs):
137
protected override void OnKeyPress(KeyPressEventArgs e)
138
{
139
base.OnKeyPress(e);
140
SimulateKeyPress(e.KeyChar);
141
e.Handled = true;
142
}
143
public void SimulateKeyPress(char ch)
144
{
145
if (Document.ReadOnly) {
146
return;
147
}
148
149
if (TextEditorProperties.UseCustomLine == true) {
150
if (SelectionManager.HasSomethingSelected) {
151
if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
152
return;
153
} else if (Document.CustomLineManager.IsReadOnly(Caret.Line, false) == true)
154
return;
155
}
156
157
if (ch < ' ') {
158
return;
159
}
160
161
if (!HiddenMouseCursor && TextEditorProperties.HideMouseCursor) {
162
HiddenMouseCursor = true;
163
Cursor.Hide();
164
}
165
CloseToolTip();
166
167
motherTextEditorControl.BeginUpdate();
168
// INSERT char
169
if (!HandleKeyPress(ch)) {
170
switch (Caret.CaretMode) {
171
case CaretMode.InsertMode:
172
InsertChar(ch);
173
break;
174
case CaretMode.OverwriteMode:
175
ReplaceChar(ch);
176
break;
177
default:
178
Debug.Assert(false, "Unknown caret mode " + Caret.CaretMode);
179
break;
180
}
181
}
182
183
int currentLineNr = Caret.Line;
184
int delta = Document.FormattingStrategy.FormatLine(this, currentLineNr, Document.PositionToOffset(Caret.Position), ch);
185
186
motherTextEditorControl.EndUpdate();
187
if (delta != 0) {
188
// this.motherTextEditorControl.UpdateLines(currentLineNr, currentLineNr);
189
}
190
}
191
//输入键通过底层的方法输入字符和更改高亮显示,如下分析(以Insert为例,取自DefaultDocument.cs):
192
public void Insert(int offset, string text)
193
{
194
if (readOnly) {
195
return;
196
}
197
OnDocumentAboutToBeChanged(new DocumentEventArgs(this, offset, -1, text));
198
DateTime time = DateTime.Now;
199
//增加字符
200
textBufferStrategy.Insert(offset, text);
201
202
time = DateTime.Now;
203
//更新LineSegment, TextWord对象
204
lineTrackingStrategy.Insert(offset, text);
205
206
time = DateTime.Now;
207
208
undoStack.Push(new UndoableInsert(this, offset, text));
209
210
time = DateTime.Now;
211
OnDocumentChanged(new DocumentEventArgs(this, offset, -1, text));
212
}
213
// 追踪到DefaultLineManager.cs中Insert()方法实际上调用到Replace(offset,length,string.Empty)方法:
214
public void Replace(int offset, int length, string text)
215
{
216
int lineNumber = GetLineNumberForOffset(offset);
217
int insertLineNumber = lineNumber;
218
if (Remove(lineNumber, offset, length)) {
219
--lineNumber;
220
}
221
222
lineNumber += Insert(insertLineNumber, offset, text);
223
224
int delta = -length;
225
if (text != null) {
226
delta = text.Length + delta;
227
}
228
229
if (delta != 0) {
230
AdaptLineOffsets(lineNumber, delta);
231
}
232
//启用高亮显示分析 ----- 注:此时的 markLines 仅存储变化的相关行,而SetContent时存储的是所有行,因此只是按需分析
233
RunHighlighter();
234
}
c, 文件字符的绘制究竟是在何处?
答:在TextView.cs中的public override void Paint(Graphics g, Rectangle rect)函数中,重要函数:PaintLinePart(),辅助绘制函数:DrawDocumentWord(), DrawBracketHighlight(), DrawSpaceMarker(), DrawVerticalRuler() 等
d, 括号匹配在何处被定义和捕捉更新?
答:TextArea.cs中的List<BracketHighlightingSheme> bracketshemes = new List<BracketHighlightingSheme>();变量存储在查找的匹配项,SearchMatchingBracket()方法中搜索并更新匹配项的显示,相关代码如下:

括号匹配
1
//引自TextArea.cs
2
List<BracketHighlightingSheme> bracketshemes = new List<BracketHighlightingSheme>();
3
Caret caret;
4
public TextArea(TextEditorControl motherTextEditorControl, TextAreaControl motherTextAreaControl)
5
{
6
// 省略无关代码
7
bracketshemes.Add(new BracketHighlightingSheme('{', '}'));
8
bracketshemes.Add(new BracketHighlightingSheme('(', ')'));
9
bracketshemes.Add(new BracketHighlightingSheme('[', ']'));
10
11
caret.PositionChanged += new EventHandler(SearchMatchingBracket);
12
// 省略无关代码
13
}
14
void SearchMatchingBracket(object sender, EventArgs e)
15
{
16
if (!TextEditorProperties.ShowMatchingBracket) {
17
textView.Highlight = null;
18
return;
19
}
20
bool changed = false;
21
if (caret.Offset == 0) {
22
if (textView.Highlight != null) {
23
int line = textView.Highlight.OpenBrace.Y;
24
int line2 = textView.Highlight.CloseBrace.Y;
25
textView.Highlight = null;
26
UpdateLine(line);
27
UpdateLine(line2);
28
}
29
return;
30
}
31
foreach (BracketHighlightingSheme bracketsheme in bracketshemes) {
32
// if (bracketsheme.IsInside(textareapainter.Document, textareapainter.Document.Caret.Offset)) {
33
Highlight highlight = bracketsheme.GetHighlight(Document, Caret.Offset - 1);
34
if (textView.Highlight != null && textView.Highlight.OpenBrace.Y >=0 && textView.Highlight.OpenBrace.Y < Document.TotalNumberOfLines) {
35
//取消旧匹配项的高亮显示
36
UpdateLine(textView.Highlight.OpenBrace.Y);
37
}
38
if (textView.Highlight != null && textView.Highlight.CloseBrace.Y >=0 && textView.Highlight.CloseBrace.Y < Document.TotalNumberOfLines) {
39
//取消旧匹配项的高亮显示
40
UpdateLine(textView.Highlight.CloseBrace.Y);
41
}
42
textView.Highlight = highlight;
43
if (highlight != null) {
44
changed = true;
45
break;
46
}
47
// }
48
}
49
if (changed || textView.Highlight != null) {
50
int line = textView.Highlight.OpenBrace.Y;
51
int line2 = textView.Highlight.CloseBrace.Y;
52
if (!changed) {
53
textView.Highlight = null;
54
}
55
//更新显示新匹配项 OpenBrace
56
UpdateLine(line);
57
//更新显示新匹配项 CloseBrace
58
UpdateLine(line2);
59
}
60
}
7、待分析的部分
本篇讨论暂未涉及如下(有价值?)内容的分析:
SyntaxHighlighting实现分析
BookmarkManager, FoldingManager, FormattingManager等
Paint()处理中坐标分析及转换
启动后,打开文档(默认支持.cs, .js, .java, .aspx等类型文件的语法高亮显示,详见ICSharpCode.TextEditor\Resources\SyntaxModes.xml)、切换语言界面如下:

切换为中文语言环境后的界面如下:

功能说明:可以实时改变语言环境;提供对常用编程语言的编辑:支持语法高亮显示、括号匹配、设置书签;尚未提供查找/替换、代码折叠、代码提示/自动完成等功能。
2、SharpDevelop的Internationalization的使用
多语言的实现就是在显示时根据键获取相应语言环境下的键值(Dictionary<key,value>),因此在编写程序时应该引用键,而非直接书写要显示的字符。一般地,应用程序的显示包括菜单、状态栏、提示字符等,因此Demo中的主菜单在配置文件(参见Basic.addin)中使用label = "${res:Menu.File.Open}",退出应用程序的提示字符使用string s = StringParser.Parse("${res:Info.Exit}");。ICSharpCode.Core.dll支持语言环境的实时修改(修改后语言设置不需重启应用程序),因此要在应用程序订阅语言环境改变的事件,相关代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

另外,可以看到Demo中的选项命令窗口也采用了插件模式来构造窗体(实现细节就不多谈了),容器窗体是TreeViewOption,插件窗体如:SelectCulturePanel, SelectStylePanel, DemoNothing,真是“扩展--无处不在”呀。SharpDevelop源码中的这些窗体的成员控件是通过.xfrm配置文件配置,窗体继承自XmlUserControl来根据配置文件生成控件,有一定的灵活性。
新的语言包资源文件放在\data\resources\目录下(如StringResources.cn-gb.resources),注意应用程序默认语言选项以及在未找到相关语言资源时都是引用Entry中的myRes.resx资源文件;语言声明文件是\data\resources\languages\LanguageDefinition.xml,其格式如下:


1

2

3

4

5

SharpPad中动态显示可选语言项的分析类是LanguageService.cs和Language.cs,此处就不多解释了。
3、SharpDevelop的Internationalization的实现分析
SharpDevelop的多语言支持的键值对是通过本地资源文件存储的,当Demo中更改语言环境设置时,引发ICSharpCode.Core.dll中的事件及方法顺序如下:
PropertyService类的属性更新引发PropertyChanged事件 -> ResourceService响应接收到的事件并重新加载保存在内存中的资源键值对,然后引发LanguageChanged事件(由使用端接收并作相关处理,如Demo中的SharpPad.cs中的事件订阅/处理)属性更新的相关代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

资源服务类的事件响应代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

4、SharpDevelop的文档管理的基本概念
代码编写工具的核心是代码编辑窗口(如打开一个.cs文件的窗口),如何在应用程序中存储该窗口内的字符内容?有些文件可能有上千行,统计下来可能有过万个字符,如果使用string对象存储字符内容,显然是不能满足性能要求,而且要实现语法高亮显示的话,还需要区分存储TextWord和相应颜色……
先看一下字符管理的基本要求:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

SharpDevelop采用的策略是使用带Gap的字符,Gap的长度小于预定规格时,按约定增加一定长度(有些类似于SqlServer的表空间管理?),插入/修改/删除字符时只是修改Gap的长度和位置和移动部分字符,核心函数如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

有了基本的数据容器,核心问题已经解决了,但是在绘制界面时直接使用上面的类,显然不够方便,于是就定义了LineSegment和TextWord类来分别存储行、单词,注意这两个类存储的只是int类型的offset和length信息,而不存储字符或字符串对象。注意TextWord类中有个HighlightColor类型的变量用以存储语法高亮显示信息。
有了上面的这些类,便可以组合起来补充些其它信息对外提供服务了,IDocument接口存在的目的即在于此,它封装了ITextEditorProperties(是否显示空格/Tab/Eol/HRuler等)、UndoStack、ITextBufferStrategy、IHighlightingStrategy、FoldingManager、BookmarkManager等属性对象。
5、SharpDevelop的SyntaxHighlighting配置文件的定义
是时候对语法高亮显示作一些分析了,此处不详述其实现,而重点分析其配置定义,从中亦可以猜出部分实现。
相关配置文件均保存在ICSharpCode.TextEditor项目\Resource\目录下
首先SyntaxMode.xml文件中显示了已定义的文件类型及其声明文件位置,其解析类参见\src\Document\HighlightingStrategy\SyntaxModes\FileSyntaxModeProvider.cs


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

取CSharp-Mode.xshd(注:xshd是Xml Syntax Highlighting Definition的缩写)为例,查看其定义:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

上面的文件公摘选了部分标签,其中
<Property>标签定义了指定名称属性的指定值
<Digits>标签定义了数值的字体显示样式
<Delimiters>定义了分隔单词的字符
<Span>定义了包含在此指定Begin/End中的字符的显示样式,注意没有<End>标签的一般设stopateol(stop at end-of-line)为true
<KeyWords>指定了其子集<Key>声明的单词的显示样式
6、SharpDevelop的TextEditor控件的实现概述
接下来的任务是要显示和支持用户输入了,直接使用TextBox或RichTextBox好像都不太现实,效率上也必定有不少损失,于是SharpDevelop的方式是直接继承自Control, Panel, UserControl 的方式来实现编辑控件(参见ICSharpCode.TextEditor项目\src\Gui\...)。
首先使用TextEditorControlBase(继承自UserControl)封装当前文件路径、Encoding、IDocument对象、ITextEditorProperties对象、快捷键列表(Dictionary<Keys, IEditAction>类型)的变量,提供LoadFile()、SaveFile()等重要方法。
TextEditorControl类继承自上面的类,并声明了Panel、Splitter、TextAreaControl、PrintDocument控件,其中显示文件的核心控件是TextAreaControl,该控件在此类中被声明了两个变量,默认只有一个primaryTextArea显示,支持切分为两个窗口的显示,当有两个窗口时,Splitter控件才有效;PrinDocument控件用以打印输出内容;该类的另一个重要功能是提供了UnDo()、Redo()方法。
TextAreaControl(继承自Panel)控件,封装了TextArea、VScrollBar、HScrollBar控件,其中TextArea负责文件数据显示,另外两个控件负责文件内容大于可见尺寸时的滚动条服务。
TextArea(继承自Control)封装了TextView, IconBarMargin, GutterMargin, FoldMargin, SelectionManager, Caret 等控件或类对象,其中前四个控件均继承自AbstractMargin,代表区域对象,各自负责字符区域(较大的文件内容绘制区)、图标区域(如Bookmark图标所在列)、Gutter区域(如行号)、折叠控制区域的绘制,SelectionManager用以控制选中项,Caret用以控制光标位置调整和显示。
下面是一些我在读代码的过程中有过的疑问及解答:
a, 加载文件时发生了什么?
答:加载文件时控件根据文件的后缀名选择了相应的高亮显示策略,然后读取文件的内容并生成相应的GapTextBufferStrategy, LineSegment, TextWord 等对象,并且对所有的TextWord对象的HighlightColor类型成员变量完成分析赋值(用以在Paint函数中显示),相关代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

b, 控件如何响应键盘事件?
答:对于方向键及快捷功能键通过预定义的实现IEditAction接口的类响应(执行功能,不影响字符内容);其它字母/数字键直接输入,同时执行更新Folding, Bookmark, Higlighting等属性信息。相关代码:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

c, 文件字符的绘制究竟是在何处?
答:在TextView.cs中的public override void Paint(Graphics g, Rectangle rect)函数中,重要函数:PaintLinePart(),辅助绘制函数:DrawDocumentWord(), DrawBracketHighlight(), DrawSpaceMarker(), DrawVerticalRuler() 等
d, 括号匹配在何处被定义和捕捉更新?
答:TextArea.cs中的List<BracketHighlightingSheme> bracketshemes = new List<BracketHighlightingSheme>();变量存储在查找的匹配项,SearchMatchingBracket()方法中搜索并更新匹配项的显示,相关代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

7、待分析的部分
本篇讨论暂未涉及如下(有价值?)内容的分析:
SyntaxHighlighting实现分析
BookmarkManager, FoldingManager, FormattingManager等
Paint()处理中坐标分析及转换