介绍
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
1 else 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
9 using System;
10 using System.Collections;
11 using System.Collections.Generic;
12 using System.Linq;
13 using System.Text;
14 using ICSharpCode.SharpDevelop.Dom;
15 using ICSharpCode.SharpDevelop.Dom.CSharp;
16 using ICSharpCode.SharpDevelop.Dom.NRefactoryResolver;
17 using ICSharpCode.SharpDevelop.Dom.VBNet;
18 using ICSharpCode.TextEditor;
19 using ICSharpCode.TextEditor.Document;
20 using ICSharpCode.TextEditor.Gui.InsightWindow;
21
22 namespace 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语言的支持。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!