改进 C# 中的代码自动完成






4.89/5 (29投票s)
一篇关于改进 C# 代码补全的文章。
引言
代码片段是 Visual Studio 的一项优秀功能,可以大大提高生产力。然而,我认为在 C# 中,它们在两个方面略有不足:
- 它们不会自动补全开括号和闭括号。
- 对于最常用的代码片段,输入以显示语句所需的击键方式不自然(需要按 Tab)。
例如,要触发 if
语句的代码片段,您必须键入“i”,然后“f”,然后“tab”,然后“tab”。输入“i”,然后“f”,然后“空格”,并出现代码片段,这肯定更好。这更自然,而且少了一次按键。我很好奇有多少百分比的 C# 程序员因为这个原因甚至不知道代码片段的存在!
幸运的是,微软通过使用扩展性来轻松地覆盖这种行为,以检查用户刚刚输入了什么文本,然后对其进行操作。
该插件做了三件事:
- 检查输入的文本是否为代码语句,如
if
、switch
、for
、foreach
,并在输入空格或换行符后运行相应的代码片段。 - 检查开括号,如
(
、[
,然后自动添加相应的闭括号。 - 在输入开花括号
{
后,将添加相应的闭花括号}
。
这些选项可以由用户配置。
背景
Visual Studio 扩展性的一个很好的背景是本网站上优秀的LineCounter项目。
安装
将演示文件解压缩到一个文件夹中。将“Jonno C# AutoComplete.AddIn”文件移动到您的“your path\My Documents\Visual Studio 2008\Addins”文件夹。打开该文件并更改以下行,以指向文件位置。
<Assembly>C:\Jonno\Jonno.AddIns.CSharp.AutoComplete\bin\
Jonno.AddIns.CSharp.AutoComplete.dll</Assembly>
如果您正在运行源代码,则“Jonno C# AutoComplete.AddIn”文件可能丢失。将其添加到插件项目中,确保将其链接到插件文件夹中的版本,而不是添加副本。
如果您想运行单元测试,您将需要 NUnit 2.5 和 Rhino Mocks 3.6。
我只在 Visual Studio 2008 上测试过此功能。
选项
可以通过菜单项 Tools->Jonno C# Auto Complete Options 加载选项窗体。
选项以 XML 文件形式保存在插件程序集所在的同一文件夹中。
窗体上有三个选项卡:Snippets(代码片段)、Brackets(括号)和 Braces(花括号)。让我们逐一研究。
代码片段
这只是一个代码片段的网格视图,将对其进行检查。如果您不喜欢某个条目,则只需删除它。如果您觉得缺少了什么,请添加它,或编辑现有条目。
网格视图中的文本是要检查的文本,字符串的最后一个字符是触发代码片段的按键。
例如,第一个条目“ if ”意味着当用户按下空格键(最后一个字符)时,它将检查前面的文本是否为“ if”。如果是,则会取消按键事件,并将 Tab 发送到窗口以触发代码片段。
条目“ if/r”将执行相同的操作,只是它会在换行符而不是空格上触发代码片段。
因此,如果您想在键入“do ”后触发 do
语句的代码片段,请在列表中添加字符串“ do ”。如果您还希望在换行符时触发它,请添加字符串“ do\r”,或者如果您不希望它触发,请将其从列表中删除。
显然,为了使此功能正常工作,您的代码片段必须具有与列表中文本匹配的快捷方式;也就是说,如果您没有快捷方式为 xxx 的代码片段,则将“ xxx ”添加到列表中不会有太大作用!
Brackets
同样,这是一个网格视图。第一列是要搜索的开括号,第二列是会自动添加的闭括号。
因此,将 (
作为第一个字符,将 )
作为第二个字符,意味着当用户按下 (
时,会自动添加一个 )
。
对于引号,第一个和第二个字符可以是相同的,例如 ' 和 ',或者 " 和 "。
如果字符不同,则仅当该行上的闭括号少于开括号时,才会添加闭括号。如果字符相同,则仅当该行上的引号数量为奇数时,才会添加闭引号。
您可以再次添加、编辑或删除以满足您的喜好。
花括号
第一个选项匹配工作方式如下(其中竖线 | 代表光标)
public string Myproperty |
按下 { 将返回
public string Myproperty { | }
而给定
public void myMethod()
|
按下 { 将返回
public void myMethod()
{
|
}
唯一真正花括号与此不同之处在于,当行上已有文本时,开花括号会产生以下结果
public void myMethod() |
按下 { 将返回
public void myMethod() {
|
}
唯一真正花括号换行选项的工作方式与上一个选项相同,区别在于闭花括号仅在用户在原始花括号后按 Enter 键后添加。
无将关闭该选项。
Using the Code
此类型项目的大部分逻辑由 VSKeyPressHelper
类处理,因此我们需要为此类提供一个属性。
private VSKeyPressHelper KeyPressHelper { get; set; }
在 Connect
类的 OnConnection
方法中,使用应用程序对象对其进行初始化,如下所示:
this.ApplicationObject = (DTE2)application;
this.AddInInstance = (AddIn)addInInst;
this.KeyPressHelper = new VSKeyPressHelper(this.ApplicationObject);
为了挂钩击键事件,我们需要一个 TextDocumentKeyPressEvents
类型的属性。我们还需要使用 WindowEvents
类型的属性来挂钩窗口事件,如下所示:
private TextDocumentKeyPressEvents TextDocKeyEvents { get; set; }
private WindowEvents WindowEvents { get; set; }
然后,在 Connect
类的 OnConnection
方法中,我们挂钩所需的窗口事件,即 WindowActivated
和 WindowCreated
事件。
Events2 events = (Events2)this.ApplicationObject.Events;
this.WindowEvents = (WindowEvents)events.get_WindowEvents(null);
this.WindowEvents.WindowActivated +=
new _dispWindowEvents_WindowActivatedEventHandler(this.WindowActivated);
this.WindowEvents.WindowCreated +=
new _dispWindowEvents_WindowCreatedEventHandler(this.WindowCreated);
当然,我们需要在 Connect
类的 OnDisconnection
方法中取消挂钩此事件。
if (this.WindowEvents != null)
{
this.WindowEvents.WindowActivated -=
new _dispWindowEvents_WindowActivatedEventHandler(this.WindowActivated);
this.WindowEvents.WindowCreated -=
new _dispWindowEvents_WindowCreatedEventHandler(this.WindowCreated);
}
在 WindowActivated
和 WindowCreated
事件的处理程序中,我们通过检查已激活窗口的最后三个字符来检查当前是否是 C# 文件。
private void WindowCreated(Window created)
{
this.AddKeyboardEventsIfFileIsaCSharpFile(created.Caption);
}
private void WindowActivated(Window gotFocus, Window lostFocus)
{
this.AddKeyboardEventsIfFileIsaCSharpFile(gotFocus.Caption);
}
private void AddKeyboardEventsIfFileIsaCSharpFile(string fileName)
{
this.RemoveKeyboardEvents();
if (fileName.EndsWith(".cs") || fileName.Contains(".cs "))
{
this.SetUpKeyboardEventsHandler();
}
}
如果它是 C# 文件,我们然后设置键盘事件的处理。这意味着该插件仅与 C# 文件一起工作,而不是在不需要的 VB.NET 中。我们对 BeforeKeyPress
和 AfterKeypress
事件感兴趣。它们首先在 RemoveKeyboardEvents
方法中被取消挂钩,然后在 SetUpKeyboardEventsHandler
方法中被添加。
Events2 events = (Events2)this.ApplicationObject.Events;
this.TextDocKeyEvents = (TextDocumentKeyPressEvents)
events.get_TextDocumentKeyPressEvents(null);
this.TextDocKeyEvents.BeforeKeyPress +=
new _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler(this.BeforeKeyPress);
this.TextDocKeyEvents.AfterKeyPress +=
new _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler(this.AfterKeyPress);
让我们深入研究 BeforeKeyPress
方法。
// This handles the code snippets checking
if (this.KeyPressHelper.CheckForCodeSnippet(selection, keypress))
{
cancelKeypress = true;
// sends escape first to exit out of intellisense if it is open
// if it is not open it does not matter.
SendKeys.Send("{esc}");
SendKeys.Send("{tab}");
}
使用击键和当前选区,CheckForCodeSnippet
确定是否刚输入了我们感兴趣的短语。如果是,则向活动窗口发送 Escape。这将取消打开的 Intellisense。然后我们向活动窗口发送 Tab,这将触发代码片段(如果存在)。
CheckForCodeSnippetStatement
方法通过从选区点向后移动来获取输入的内容,然后将选区点移回原处,如下所示:
private string GetPreviousTextFromSelectionPoint(EditPoint ep, EditPoint sp, int length)
{
sp.CharLeft(length);
var text = sp.GetText(ep);
sp.CharRight(length);
return text;
}
然后,它将文本与“ if”进行比较,以查看是否输入了“ if
”语句。
AfterKeyPress
方法通过操作选区周围的文本以类似方式工作。
switch (keypress)
{
case "{":
// handles normal terse brackets
this.KeyPressHelper.AddEndBraceAfterOpenBrace(selection);
break;
default:
// handles all other brackets
this.KeyPressHelper.AddEndBracketAfterOpenBracket(selection, keypress);
break;
}
主要区别在于文本是如何操作的。我们不是向活动窗口发送按键,而是可以直接插入文本,使用选区和编辑点,例如插入结束括号的代码:
sp.Insert(reverse);
selection.CharLeft(false, 1);
第一行插入括号,然后选区向左移动。VSKeyPressHelper
类中的其余大部分代码都基于类似的原理。
Connect
类中的许多代码涉及工具窗口和菜单的创建,我将不再详述。其他值得注意的类是 XMLHelper
类,它将设置保存和加载到/从 XML 文件,以及 OptionsView
,它是用于设置选项的工具窗口。
关注点
这类项目的第一件令人恼火的事情是自动化单元测试非常困难!在选区周围移动并插入文本 thus 需要一些试错才能获得正确的结果。我已添加单元测试(如果可能),我认为测试 Connect
类或 VSKeyPressHelper
类不值得付出努力。
我做的最愚蠢的事情是没有考虑到注释!在使用了一段时间后,我添加了 LineContainsCSharpComments
方法,该方法检查输入的文本前面是否有“//”,如果有则关闭该行为。
历史
- 2009 年 9 月 7 日:初始版本。
- 2009 年 9 月 14 日:添加了对代码片段、括号和花括号样式的配置。在实际可行的地方为源代码添加了单元测试。重构了代码库。
- 2009 年 9 月 15 日:添加了对
WindowCreated
事件的处理。