介绍
SharpDevelop的源代码里自带一个CSharp代码自动完成功能(Code Completion)的例子。如下图所示:
图1. 代码完成
看上去似乎好像挺不好做的,理论上要做词法分析、语法分析,还要解析一些如mscorlib之类的DLL。但是事实上SharpDevelop已经为我们做了这些,上面的例子只要写几个类就可以完成。整个Solution如下图所示:
图2. 代码完成例子的Solution
图3. Code Metrics Results
直接使用SharpDevelop里的分析器,就可以很简单地完成Code Completion的功能。平均每个文件只有100行的代码。也许有人要说这样没有意思,但是我们不应该“重新做个轮子”对吗?而且就算要深入地学习,也要先来把这个例子弄明白的。
这个例子里的Code Completion并不象SharpDevelop里的那么完成,没有提供下面的功能。
图4. 缺失的ToolTip提示
在输入左括号时,应该在ToolTip中给出这个函数的所有重载,但是这个例子里并没有给出这个功能的实现。下面在介绍代码完成的同时,会给出这个ToolTip的实现。
博客园的Michael Zhang在其SharpDevelop浅析系列文章中也介绍了代码完成功能。如果对整个SharpDevelop有兴趣可以参考一下。下面进入正题……
第一部分 Code Completion的实现
下图是这个例子里的类体系
图5. 整个例子的类体系
从类的结构来看,其实与SharpDevelop本身的实现不是一个层次的,这个类结构的耦合性很强,还好要看的是如何实现Code Completion,而不是它的设计。要看设计,还是看SharpDevelop好了。
下面逐个介绍一下里面的类。
l CodeCompletionData类:是用于表示代码完成列表中每个项的Visual Object。看看它的父类会更明显一些。
图6. DefaultCompletionData类图
l MainForm类:就是看到的整个窗体。里面有三个控件,一个是SharpDevelop的TextEditorControl,一个状态栏,一个ImageList。主窗体加载之后会开启一个Parser线程,每2秒对整个文档做一次Parse。(汗,用Stopwatch测试了一下,对一个很小的文件的一次Parse要50ms左右。)Parse之后,用于做Code Completion的数据就有了。在哪儿?没去认真找过。应该是在ProjectContent里。下面是Parse的步骤:
Code
1 void ParseStep()
2 {
3 string code = null;
4 Invoke(new MethodInvoker(delegate {
5 code = textEditorControl1.Text;
6 }));
7 TextReader textReader = new StringReader(code);
8 Dom.ICompilationUnit newCompilationUnit;
9 NRefactory.SupportedLanguage supportedLanguage;
10 if (IsVisualBasic)
11 supportedLanguage = NRefactory.SupportedLanguage.VBNet;
12 else
13 supportedLanguage = NRefactory.SupportedLanguage.CSharp;
14 using (NRefactory.IParser p = NRefactory.ParserFactory.CreateParser(supportedLanguage, textReader)) {
15 // we only need to parse types and method definitions, no method bodies
16 // so speed up the parser and make it more resistent to syntax
17 // errors in methods
18 p.ParseMethodBodies = false;
19
20 p.Parse();
21 newCompilationUnit = ConvertCompilationUnit(p.CompilationUnit);
22 }
23 // Remove information from lastCompilationUnit and add information from newCompilationUnit.
24 myProjectContent.UpdateCompilationUnit(lastCompilationUnit, newCompilationUnit, DummyFileName);
25 lastCompilationUnit = newCompilationUnit;
26 parseInformation.SetCompilationUnit(newCompilationUnit);
27 }
l CodeCompletionKeyHandler类:看名字就知道,就是针对CodeCompletion处理窗体的键盘事件的,如果点了”.”,就实例化一个CodeCompletionProvider并显示CodeCompletion窗口。按VS的分析,这个只有22行代码的文件就不细介绍了。
ShowCodeCompletionList
1 if (key == '.') {
2 ICompletionDataProvider completionDataProvider = new CodeCompletionProvider(mainForm);
3
4 codeCompletionWindow = CodeCompletionWindow.ShowCompletionWindow(
5 mainForm, // The parent window for the completion window
6 editor, // The text editor to show the window for
7 MainForm.DummyFileName, // Filename - will be passed back to the provider
8 completionDataProvider, // Provider to get the list of possible completions
9 key // Key pressed - will be passed to the provider
10 );
11 if (codeCompletionWindow != null) {
12 // ShowCompletionWindow can return null when the provider returns an empty list
13 codeCompletionWindow.Closed += new EventHandler(CloseCodeCompletionWindow);
14 }
15 }
l HostCallbackImplementation类:这个更少,就13行代码。做的事件就是后台分析出错的时候,把消息显示出来。
l CodeCompletionProvider类:虽然最重要的部分,却也只有51行代码。主要完成了两件事情:1. 生成CodeCompletion列表。2. 用户触发一个Item的时候把相应的内容插入(就2行)。
生成CodeCompletion列表的功能也十分简单,大致分成三个步骤:
a. FindExpression:就是找到当前用户的点”.”之前的那个东西到底是个什么东西。
b. Resolve:得到找到的那个东西的Memeber列表。
c. GenerateData:从得到的列表生成用于在UI上显示的CodeCompletionData。
l ToolTipProvider类:这个可不是那个缺失的功能哦。这个只是在MouseOver一个Member时,显示出一些信息。在图1的上面的那个就是了。(好像ToolTip还错了……)这个过程和CodeCompletionProvider大致相同。就不再赘述了。
这样,整个CodeCompletion的功能就完成了。整个例子359行代码。(仔细看看代码,你会有信心把它到缩减到200行。)
第二部分 添加Code Insight功能
什么是Code Insight?就是之前提到的缺失的ToolTip啊。在SharpDevelop里给了它一个更专业的名字叫“Code Insight”。
一向奉行“Don’t recreate the wheel”,这次也不例外。Sample没有,SharpDevelop有啊。Ctrl+V过来不就行了?(当然商业项目不行哦,连Sample也不行。人家是GPL)
然后就在SharpDevelop的源代码中找到了一个名为MethodInsightDataProvider的文件(在ICSharpCode.SharpDevelop.DefaultEditor.Gui.Editor名字空间下)加了进来,然后在KeyHandler里加入对InsightWindow的支持。
ShowInsightWindow
1else if (key == '(')
2{
3 IInsightDataProvider insightDataProvider = new MethodInsightDataProvider(mainForm);
4
5 InsightWindow insightWindow = new InsightWindow(mainForm, editor);
6 insightWindow.AddInsightDataProvider(insightDataProvider, MainForm.DummyFileName);
7 insightWindow.ShowInsightWindow();
8}
再把SharpDevelop里的CodeCompletionData里的GetDocumentation函数也Copy到这个Sample的CodeCompletionData类中。
这样所有的类的准备完了。
然而在MethodInsightDataProvider中使用到的ParserService、AmbienceService、LoggingService和MessageService都没有找到引用。又去看了一下SharpDevelop的运行流程,找到了开启这几个Service的地方,发现差不多也已经同时把整个IDE开启了。这个可不是我想要的。而且很多类都是protected sealed类。从外界是Call不到的。看来必须要”Recreate the wheel”,把那个DataProvider重新写一个了。还好那个Provider只有100多行代码,自己写一个并不复杂。把LoggingService和MessageService的代码都删除。而ParserService和AmbienceService的作用不过是Parser和Ambience的工厂类,自己实例化一个不就得了?这样,DataProvider中所引用的所有Service都被剥离了。
整个MethodInsightDataProvider的代码如下所示:
MethodInsightDataProvider
1// <file>
2// <copyright see="prj:///doc/copyright.txt"/>
3// <license see="prj:///doc/license.txt"/>
4// <owner name="Mike Krüger" email="mike@icsharpcode.net"/>
5// <modifier name="Hugo Gu" email="nankezhishi@gmail.com"
6// <version>$Revision: 3105 $</version>
7// </file>
8
9using System;
10using System.Collections;
11using System.Collections.Generic;
12using System.Linq;
13using System.Text;
14using ICSharpCode.SharpDevelop.Dom;
15using ICSharpCode.SharpDevelop.Dom.CSharp;
16using ICSharpCode.SharpDevelop.Dom.NRefactoryResolver;
17using ICSharpCode.SharpDevelop.Dom.VBNet;
18using ICSharpCode.TextEditor;
19using ICSharpCode.TextEditor.Document;
20using ICSharpCode.TextEditor.Gui.InsightWindow;
21
22namespace CSharpEditor
23{
24 class MethodInsightDataProvider : IInsightDataProvider
25 {
26 MainForm mainForm;
27 string fileName = null;
28 IDocument document = null;
29 TextArea textArea = null;
30 protected List<IMethodOrProperty> methods = new List<IMethodOrProperty>();
31
32 public List<IMethodOrProperty> Methods
33 {
34 get
35 {
36 return methods;
37 }
38 }
39
40 public int InsightDataCount
41 {
42 get
43 {
44 return methods.Count;
45 }
46 }
47
48 int defaultIndex = -1;
49
50 public int DefaultIndex
51 {
52 get
53 {
54 return defaultIndex;
55 }
56 set
57 {
58 defaultIndex = value;
59 }
60 }
61
62 public string GetInsightData(int number)
63 {
64 IMember method = methods[number];
65 IAmbience conv = MainForm.IsVisualBasic ? (IAmbience)new VBNetAmbience() : new CSharpAmbience();
66 conv.ConversionFlags = ConversionFlags.StandardConversionFlags | ConversionFlags.UseFullyQualifiedMemberNames;
67 string documentation = method.Documentation;
68 string text = conv.Convert(method);
69 return text + "\n" + CodeCompletionData.GetDocumentation(documentation);
70 }
71
72 int lookupOffset = -1;
73 bool setupOnlyOnce;
74
75 /**//// <summary>
76 ///
77 /// </summary>
78 /// <param name="mainForm"></param>
79 public MethodInsightDataProvider(MainForm mainForm)
80 {
81 this.mainForm = mainForm;
82 }
83
84 /**//// <summary>
85 /// Creates a MethodInsightDataProvider looking at the specified position.
86 /// </summary>
87 public MethodInsightDataProvider(int lookupOffset, bool setupOnlyOnce)
88 {
89 this.lookupOffset = lookupOffset;
90 this.setupOnlyOnce = setupOnlyOnce;
91 }
92
93 int initialOffset;
94
95 public void SetupDataProvider(string fileName, TextArea textArea)
96 {
97 if (setupOnlyOnce && this.textArea != null) return;
98 IDocument document = textArea.Document;
99 this.fileName = fileName;
100 this.document = document;
101 this.textArea = textArea;
102 int useOffset = (lookupOffset < 0) ? textArea.Caret.Offset : lookupOffset;
103 initialOffset = useOffset;
104
105 IExpressionFinder expressionFinder;
106 if (MainForm.IsVisualBasic)
107 {
108 expressionFinder = new VBExpressionFinder();
109 }
110 else
111 {
112 expressionFinder = new CSharpExpressionFinder(mainForm.parseInformation);
113 }
114
115 ExpressionResult expressionResult;
116 if (expressionFinder == null)
117 expressionResult = new ExpressionResult(TextUtilities.GetExpressionBeforeOffset(textArea, useOffset));
118 else
119 expressionResult = expressionFinder.FindExpression(textArea.Document.TextContent, useOffset);
120
121 if (expressionResult.Expression == null) // expression is null when cursor is in string/comment
122 return;
123 expressionResult.Expression = expressionResult.Expression.Trim();
124
125 int caretLineNumber = document.GetLineNumberForOffset(useOffset);
126 int caretColumn = useOffset - document.GetLineSegment(caretLineNumber).Offset;
127 // the parser works with 1 based coordinates
128 SetupDataProvider(fileName, document, expressionResult, caretLineNumber + 1, caretColumn + 1);
129 }
130
131 protected virtual void SetupDataProvider(string fileName, IDocument document, ExpressionResult expressionResult, int caretLineNumber, int caretColumn)
132 {
133 bool constructorInsight = false;
134 if (expressionResult.Context == ExpressionContext.Attribute)
135 {
136 constructorInsight = true;
137 }
138 else if (expressionResult.Context.IsObjectCreation)
139 {
140 constructorInsight = true;
141 expressionResult.Context = ExpressionContext.Type;
142 }
143 else if (expressionResult.Context == ExpressionContext.BaseConstructorCall)
144 {
145 constructorInsight = true;
146 }
147
148 NRefactoryResolver resolver = new NRefactoryResolver(mainForm.myProjectContent.Language);
149
150 ResolveResult results = resolver.Resolve(expressionResult, mainForm.parseInformation, document.TextContent);
151 LanguageProperties language = mainForm.myProjectContent.Language;
152 TypeResolveResult trr = results as TypeResolveResult;
153 if (trr == null && language.AllowObjectConstructionOutsideContext)
154 {
155 if (results is MixedResolveResult)
156 trr = (results as MixedResolveResult).TypeResult;
157 }
158 if (trr != null && !constructorInsight)
159 {
160 if (language.AllowObjectConstructionOutsideContext)
161 constructorInsight = true;
162 }
163 if (constructorInsight)
164 {
165 if (trr == null)
166 {
167 if ((expressionResult.Expression == "this") && (expressionResult.Context == ExpressionContext.BaseConstructorCall))
168 {
169 methods.AddRange(GetConstructorMethods(results.ResolvedType.GetMethods()));
170 }
171
172 if ((expressionResult.Expression == "base") && (expressionResult.Context == ExpressionContext.BaseConstructorCall))
173 {
174 if (results.CallingClass.BaseType.DotNetName == "System.Object")
175 return;
176 methods.AddRange(GetConstructorMethods(results.CallingClass.BaseType.GetMethods()));
177 }
178 }
179 else
180 {
181 methods.AddRange(GetConstructorMethods(trr.ResolvedType.GetMethods()));
182
183 if (methods.Count == 0 && trr.ResolvedClass != null && !trr.ResolvedClass.IsAbstract && !trr.ResolvedClass.IsStatic)
184 {
185 // add default constructor
186 methods.Add(Constructor.CreateDefault(trr.ResolvedClass));
187 }
188 }
189 }
190 else
191 {
192 MethodGroupResolveResult result = results as MethodGroupResolveResult;
193 if (result == null)
194 return;
195 bool classIsInInheritanceTree = false;
196 if (result.CallingClass != null)
197 classIsInInheritanceTree = result.CallingClass.IsTypeInInheritanceTree(result.ContainingType.GetUnderlyingClass());
198
199 foreach (IMethod method in result.ContainingType.GetMethods())
200 {
201 if (language.NameComparer.Equals(method.Name, result.Name))
202 {
203 if (method.IsAccessible(result.CallingClass, classIsInInheritanceTree))
204 {
205 methods.Add(method);
206 }
207 }
208 }
209 if (methods.Count == 0 && result.CallingClass != null && language.SupportsExtensionMethods)
210 {
211 ArrayList list = new ArrayList();
212 ResolveResult.AddExtensions(language, list, result.CallingClass, result.ContainingType);
213 foreach (IMethodOrProperty mp in list)
214 {
215 if (language.NameComparer.Equals(mp.Name, result.Name) && mp is IMethod)
216 {
217 DefaultMethod m = (DefaultMethod)mp.CreateSpecializedMember();
218 // for the insight window, remove first parameter and mark the
219 // method as normal - this is required to show the list of
220 // parameters the method expects.
221 m.IsExtensionMethod = false;
222 m.Parameters.RemoveAt(0);
223 methods.Add(m);
224 }
225 }
226 }
227 }
228 }
229
230 List<IMethodOrProperty> GetConstructorMethods(List<IMethod> methods)
231 {
232 List<IMethodOrProperty> constructorMethods = new List<IMethodOrProperty>();
233 foreach (IMethod method in methods)
234 {
235 if (method.IsConstructor && !method.IsStatic)
236 {
237 constructorMethods.Add(method);
238 }
239 }
240 return constructorMethods;
241 }
242
243 public bool CaretOffsetChanged()
244 {
245 bool closeDataProvider = textArea.Caret.Offset <= initialOffset;
246 int brackets = 0;
247 int curlyBrackets = 0;
248 if (!closeDataProvider)
249 {
250 bool insideChar = false;
251 bool insideString = false;
252 for (int offset = initialOffset; offset < Math.Min(textArea.Caret.Offset, document.TextLength); ++offset)
253 {
254 char ch = document.GetCharAt(offset);
255 switch (ch)
256 {
257 case '\'':
258 insideChar = !insideChar;
259 break;
260 case '(':
261 if (!(insideChar || insideString))
262 {
263 ++brackets;
264 }
265 break;
266 case ')':
267 if (!(insideChar || insideString))
268 {
269 --brackets;
270 }
271 if (brackets <= 0)
272 {
273 return true;
274 }
275 break;
276 case '"':
277 insideString = !insideString;
278 break;
279 case '}':
280 if (!(insideChar || insideString))
281 {
282 --curlyBrackets;
283 }
284 if (curlyBrackets < 0)
285 {
286 return true;
287 }
288 break;
289 case '{':
290 if (!(insideChar || insideString))
291 {
292 ++curlyBrackets;
293 }
294 break;
295 case ';':
296 if (!(insideChar || insideString))
297 {
298 return true;
299 }
300 break;
301 }
302 }
303 }
304
305 return closeDataProvider;
306 }
307
308 public bool CharTyped()
309 {
310 return false;
311 }
312 }
313}
314
运行一下。
图7. Method Insight
大功告成。
其实还有一种Insight——IndexerInsight。就是在输入‘[’的时候给出ToolTip提示。这个就留给有兴趣的读者吧。
/Files/nankezhishi/source/CSharpCodeCompletion.zip这里是改版的代码完成的所有代码。
在下一篇中,将为这个示例添加Boo语言的支持。