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
3public 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控件)
13string 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
23void 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
43using System;
44namespace 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
3public 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}
14protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
15{
16 if (PropertyChanged != null) {
17 PropertyChanged(this, e);
18 }
19}
20public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
21public event PropertyChangedEventHandler PropertyChanged;
资源服务类的事件响应代码如下:
ResourceService类的事件响应
1//引自ICSharpCode.Core\src\Services\ResourceService\ResourceService.cs
2//首先在类的初始化中订阅属性更改事件:
3PropertyService.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChange);
4//类级别变量:
5static Hashtable localStrings = null;
6static Hashtable localIcons = null;
7public static event EventHandler LanguageChanged;
8//事件响应:
9static 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}
17static 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}
52static Hashtable Load(string name, string language)
53{
54 return Load(resourceDirectory + Path.DirectorySeparatorChar + name + "." + language + ".resources");
55}
56static 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
2namespace 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//类级别变量:
3char[] buffer = new char[0];
4int gapBeginOffset = 0;
5int gapEndOffset = 0;
6/// <summary>
7/// 小于此长度时增长Buffer/Gap
8/// </summary>
9int minGapLength = 32;
10/// <summary>
11/// 每次需要增长时,增长此长度的Gap
12/// </summary>
13int maxGapLength = 256;
14//重要函数:
15public void SetContent(string text)
16{
17 if (text == null) {
18 text = String.Empty;
19 }
20 buffer = text.ToCharArray();
21 gapBeginOffset = gapEndOffset = 0;
22}
23public void Insert(int offset, string text)
24{
25 Replace(offset, 0, text);
26}
27public void Remove(int offset, int length)
28{
29 Replace(offset, length, String.Empty);
30}
31public 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}
43void 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
2public 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
32ITextBufferStrategy textBufferStrategy = null;
33ILineManager lineTrackingStrategy = null;
34public 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
52public 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):
2protected Dictionary<Keys, IEditAction> editactions = new Dictionary<Keys, IEditAction>();
3protected TextEditorControlBase()
4{
5 GenerateDefaultActions();
6 HighlightingManager.Manager.ReloadSyntaxHighlighting += new EventHandler(ReloadHighlighting);
7}
8void 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}
70internal IEditAction GetEditAction(Keys keyData)
71{
72 if (!editactions.ContainsKey(keyData)) {
73 return null;
74 }
75 return (IEditAction)editactions[keyData];
76}
77//功能键的响应(取自TextArea.cs):
78protected override bool ProcessDialogKey(Keys keyData)
79{
80 return ExecuteDialogKey(keyData) || base.ProcessDialogKey(keyData);
81}
82public 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):
137protected override void OnKeyPress(KeyPressEventArgs e)
138{
139 base.OnKeyPress(e);
140 SimulateKeyPress(e.KeyChar);
141 e.Handled = true;
142}
143public 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):
192public 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)方法:
214public 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
2List<BracketHighlightingSheme> bracketshemes = new List<BracketHighlightingSheme>();
3Caret caret;
4public 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}
14void 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//a, 设置语言
2//引自SharpPad项目的Dialogs\SelectCulturePanel.cs
3public 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控件)
13string 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
23void 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
43using System;
44namespace 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
3public 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}
14protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
15{
16 if (PropertyChanged != null) {
17 PropertyChanged(this, e);
18 }
19}
20public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
21public event PropertyChangedEventHandler PropertyChanged;
资源服务类的事件响应代码如下:
ResourceService类的事件响应
1//引自ICSharpCode.Core\src\Services\ResourceService\ResourceService.cs
2//首先在类的初始化中订阅属性更改事件:
3PropertyService.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChange);
4//类级别变量:
5static Hashtable localStrings = null;
6static Hashtable localIcons = null;
7public static event EventHandler LanguageChanged;
8//事件响应:
9static 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}
17static 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}
52static Hashtable Load(string name, string language)
53{
54 return Load(resourceDirectory + Path.DirectorySeparatorChar + name + "." + language + ".resources");
55}
56static 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
2namespace 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//类级别变量:
3char[] buffer = new char[0];
4int gapBeginOffset = 0;
5int gapEndOffset = 0;
6/// <summary>
7/// 小于此长度时增长Buffer/Gap
8/// </summary>
9int minGapLength = 32;
10/// <summary>
11/// 每次需要增长时,增长此长度的Gap
12/// </summary>
13int maxGapLength = 256;
14//重要函数:
15public void SetContent(string text)
16{
17 if (text == null) {
18 text = String.Empty;
19 }
20 buffer = text.ToCharArray();
21 gapBeginOffset = gapEndOffset = 0;
22}
23public void Insert(int offset, string text)
24{
25 Replace(offset, 0, text);
26}
27public void Remove(int offset, int length)
28{
29 Replace(offset, length, String.Empty);
30}
31public 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}
43void 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
2public 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
32ITextBufferStrategy textBufferStrategy = null;
33ILineManager lineTrackingStrategy = null;
34public 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
52public 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):
2protected Dictionary<Keys, IEditAction> editactions = new Dictionary<Keys, IEditAction>();
3protected TextEditorControlBase()
4{
5 GenerateDefaultActions();
6 HighlightingManager.Manager.ReloadSyntaxHighlighting += new EventHandler(ReloadHighlighting);
7}
8void 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}
70internal IEditAction GetEditAction(Keys keyData)
71{
72 if (!editactions.ContainsKey(keyData)) {
73 return null;
74 }
75 return (IEditAction)editactions[keyData];
76}
77//功能键的响应(取自TextArea.cs):
78protected override bool ProcessDialogKey(Keys keyData)
79{
80 return ExecuteDialogKey(keyData) || base.ProcessDialogKey(keyData);
81}
82public 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):
137protected override void OnKeyPress(KeyPressEventArgs e)
138{
139 base.OnKeyPress(e);
140 SimulateKeyPress(e.KeyChar);
141 e.Handled = true;
142}
143public 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):
192public 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)方法:
214public 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
2List<BracketHighlightingSheme> bracketshemes = new List<BracketHighlightingSheme>();
3Caret caret;
4public 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}
14void 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()处理中坐标分析及转换