65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (29投票s)

2009年9月8日

CPOL

7分钟阅读

viewsIcon

67459

downloadIcon

1365

一篇关于改进 C# 代码补全的文章。

Sample Image

引言

代码片段是 Visual Studio 的一项优秀功能,可以大大提高生产力。然而,我认为在 C# 中,它们在两个方面略有不足:

  • 它们不会自动补全开括号和闭括号。
  • 对于最常用的代码片段,输入以显示语句所需的击键方式不自然(需要按 Tab)。

例如,要触发 if 语句的代码片段,您必须键入“i”,然后“f”,然后“tab”,然后“tab”。输入“i”,然后“f”,然后“空格”,并出现代码片段,这肯定更好。这更自然,而且少了一次按键。我很好奇有多少百分比的 C# 程序员因为这个原因甚至不知道代码片段的存在!

幸运的是,微软通过使用扩展性来轻松地覆盖这种行为,以检查用户刚刚输入了什么文本,然后对其进行操作。

该插件做了三件事:

  1. 检查输入的文本是否为代码语句,如 ifswitchforforeach,并在输入空格或换行符后运行相应的代码片段。
  2. 检查开括号,如 ([,然后自动添加相应的闭括号。
  3. 在输入开花括号 { 后,将添加相应的闭花括号 }

这些选项可以由用户配置。

背景

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(花括号)。让我们逐一研究。

代码片段

Sample Image

这只是一个代码片段的网格视图,将对其进行检查。如果您不喜欢某个条目,则只需删除它。如果您觉得缺少了什么,请添加它,或编辑现有条目。

网格视图中的文本是要检查的文本,字符串的最后一个字符是触发代码片段的按键。

例如,第一个条目“ if ”意味着当用户按下空格键(最后一个字符)时,它将检查前面的文本是否为“ if”。如果是,则会取消按键事件,并将 Tab 发送到窗口以触发代码片段。

条目“ if/r”将执行相同的操作,只是它会在换行符而不是空格上触发代码片段。

因此,如果您想在键入“do ”后触发 do 语句的代码片段,请在列表中添加字符串“ do ”。如果您还希望在换行符时触发它,请添加字符串“ do\r”,或者如果您不希望它触发,请将其从列表中删除。

显然,为了使此功能正常工作,您的代码片段必须具有与列表中文本匹配的快捷方式;也就是说,如果您没有快捷方式为 xxx 的代码片段,则将“ xxx ”添加到列表中不会有太大作用!

Brackets

Sample Image

同样,这是一个网格视图。第一列是要搜索的开括号,第二列是会自动添加的闭括号。

因此,将 ( 作为第一个字符,将 ) 作为第二个字符,意味着当用户按下 ( 时,会自动添加一个 )

对于引号,第一个和第二个字符可以是相同的,例如 ' 和 ',或者 " 和 "。

如果字符不同,则仅当该行上的闭括号少于开括号时,才会添加闭括号。如果字符相同,则仅当该行上的引号数量为奇数时,才会添加闭引号。

您可以再次添加、编辑或删除以满足您的喜好。

花括号

Sample Image

第一个选项匹配工作方式如下(其中竖线 | 代表光标)

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 方法中,我们挂钩所需的窗口事件,即 WindowActivatedWindowCreated 事件。

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);
}

WindowActivatedWindowCreated 事件的处理程序中,我们通过检查已激活窗口的最后三个字符来检查当前是否是 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 中。我们对 BeforeKeyPressAfterKeypress 事件感兴趣。它们首先在 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 事件的处理。
© . All rights reserved.