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

C# 和 VB.NET 代码搜索器 - 使用 Roslyn

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (51投票s)

2012 年 7 月 14 日

LGPL3

14分钟阅读

viewsIcon

218202

downloadIcon

5716

使用 Roslyn 的快速 C# 和 VB.NET 代码搜索器。

目录

引言

本文介绍了一个使用 Roslyn 的工具,该工具可以以 4 种方式搜索大型代码库。

  1. 搜索方法中的文本
  2. 搜索对特定方法的调用  
  3. 按名称搜索方法
  4. 按名称搜索属性
  5. 按名称搜索类

截图 

这是 C# 和 VB.NET 代码搜索器运行时的截图。

 

问题  

最近我有一个任务,需要在一个大型遗留代码库(61 个解决方案,C# 代码)中进行大量的源代码搜索。需要将一个字段从一个表移动到另一个表。这是一个会影响代码库某些部分的操作。为了找出影响范围,我需要在数据层找到调用了存储过程的方法。然后我必须自下而上地遍历代码库,查看这些方法在哪里被调用,以及对代码的影响是什么。

起初我使用了免费工具 "TextCrawler 2"(http://www.digitalvolcano.co.uk/content/index.php)。这是一个相当快速的文本搜索工具。但问题是,它对 C# 语言一无所知。例如,如果你搜索对某个方法的调用,TextCrawler 会愉快地为你找到包含被注释掉的方法调用文件的文件。另一个问题是,它不够快(搜索 61 个解决方案可能需要一些时间。)。我还使用了 Microsoft Desktop Search 工具,它速度很快,但同样对源代码不够“智能”。

自从我读到 Roslyn 后,我一直在思考如何将其用于此目的。

关于 Roslyn 的背景

Roslyn 是微软的一个项目,旨在通过 API 开放在 VB 和 C# 编译器,并提供对编译器在编译过程不同阶段收集的信息的便捷访问。要开始了解 Roslyn,你可以阅读它在这里:

或者,如果你想深入了解 Roslyn,这里有一份微软的白皮书:

本文无意提供 Roslyn 的入门介绍,有一些不错的 CodeProject 文章已经做到了这一点。

我还发现,在安装了 Microsoft Roslyn CTP - June 2012 后,我的文档文件夹中安装了许多示例项目。

解决方案

所以我尝试使用 Roslyn,看看能否创建一个可以更快地搜索代码的工具。我认为我成功了。我现在一直在使用它!我创建了一个 Windows Forms 应用程序,它有 5 种方法可以搜索 C# 和 VB.NET 代码:

  1. 搜索方法中的文本 
  2. 搜索对特定方法的调用
  3. 按名称搜索方法
  4. 按名称搜索属性
  5. 按名称搜索类 

我决定与大家分享这个工具,以便每个人都能享受到它。通过发布这篇文章,我希望:

  • 人们也能发现它很有用。
  • 我能获得有价值的反馈,以便改进工具。
  • 人们能以我未曾想过的方式扩展/改编该工具或其部分功能。

为什么要使用它? 

例如:“转到定义”功能,用于其他解决方案

假设您正在调试一个会话。您正在调试解决方案 X,它调用了位于另一个解决方案 Y 中的服务。现在您看到一个类中调用了一个方法,该类位于解决方案 Y 中。在 Visual Studio 中,您可以通过右键单击 - “转到定义”或按 F12 来转到方法的定义。但当方法位于另一个解决方案时,这是无法做到的!因此,如果您想查找方法的定义,唯一的方法是:

  1. 在调试会话中单步执行到该方法
  2. 打开解决方案 Y 并找到您想要查看的方法。

使用 RoslynCodeSearcher,可以非常轻松地查找位于另一个解决方案中的方法,只需在搜索字段中键入其名称,选择“搜索方法”并单击 [搜索] 即可。

作为重构的辅助

有时您会想知道“如果我删除这个方法,它会在哪个解决方案的丛林中被调用?”。您可以进行文本搜索,或者可以开始编译构建来查看哪里会出错,但对于拥有许多解决方案的某些项目,完整的编译构建会花费很长时间。使用 RoslynCodeSearcher,只需在搜索字段中键入方法的名称,选择“搜索调用”并单击 [搜索]。稍等片刻,搞定!

为什么不使用反射来实现?

我不使用反射的原因是,我希望能够访问我搜索的解决方案的实际源代码。例如,我想返回方法体。反射做不到这一点,它只能处理元数据(类的类型、方法的名称和签名等)。另外,当我想对方法中的文本片段进行文本搜索时,使用 Roslyn 比搜索大量解决方案的文本搜索更快。这是因为解决方案是在内存中编译的。

它有多快  

第一次使用时,该工具会比较慢,因为它需要在内存中编译解决方案(在我这里是 604 MB 内存)。这些已编译的解决方案将以 IWorkspace 对象的形式可用。每次启动工具时都会发生这种情况。进度指示器会显示编译的进度。对于少量解决方案,这个编译过程一两秒钟即可完成。对于大量的解决方案,则需要更长的时间。作为参考:在我的电脑上,第一次编译 61 个解决方案到内存中大约需要半分钟。初始编译完成后,搜索速度会非常快:搜索 61 个解决方案大约需要一到几秒钟,具体取决于找到多少结果。这是因为已经有了内存中的 IWorkspace 对象列表。自从我开始使用 .NET 4 的 Parallel.ForEach 关键字以来,性能得到了显著提高(提高的幅度取决于您计算机的处理器核心数,双核、四核等)。

如何使用它   

必备组件

请确保您已按顺序安装了以下软件,**否则解决方案将无法构建**:

本文是为 Roslyn June 2012 CTP 版本撰写的,该版本与 Visual Studio 2010 SP1 兼容。但是,新版本的 Roslyn,即 September 2012 CTP 版本,仅与 Visual Studio 2012 兼容。我已添加了与 Visual Studio 2012 兼容的代码搜索器源代码的下载链接。本文的其余部分仍需要更新以反映这一事实(或者我将为 Visual Studio 2012 版本专门写一篇新文章,我还没有决定)。

如果您有 Visual Studio 2012,则需要按以下顺序安装软件

下一步:solutions.txt 文件 

您必须向该工具提供要搜索的解决方案列表。

您可以通过以下两种方式完成此操作:

  1. 在可执行文件的目录中(或构建解决方案后的 \bin\debug 目录)放置一个名为 "solutions.txt" 的文本文件。该工具将在启动时读取它(如果存在)。此文本文件应包含解决方案的完整路径,每行一个。
  2. 如果 solutions.txt 文件尚不存在,请点击 [Browse ...],然后在文件对话框中选择一个目录。接下来点击 [Update solution List]。该工具将从选定的目录开始,递归地向下遍历目录结构,查找解决方案 (.sln) 文件。

结果将存储在可执行文件目录中的 "solutions.txt" 文件中。现有的 "solutions.txt" 文件将被覆盖。 

下一步:搜索 

  • 在文本框中输入您要搜索的文本。
  • 通过单击其中一个单选按钮来选择一种搜索方式。
  • 单击 [Search]。

将搜索 solutions.txt 中的解决方案、所有底层项目以及所有底层源文件。

搜索结果包括:

  1. 包含找到方法的源文件的路径。 
  2. 方法的正文。 

包含/排除文件

您还可以指定右侧文本框中的单词:

"Do not include files containing words in filename. Separate by comma."

"Only include files containing word in filename. Separate by comma."

  • "Do not include"(不包含)表示,该工具将不搜索文件名中包含任何指定词语的代码文件。
  • "Only include"(仅包含)表示,该工具将只搜索文件名中包含任何指定词语的代码文件。  

这些文本框是互斥的,不能同时使用,“Do not include”优先于“Only include”。

搜索部分文本

您可以只键入要搜索文本的一部分。例如,如果您想搜索所有包含“Save”这个词的方法,如“SaveCustomer”、“SaveOrder”,则勾选“Search part of text”(搜索部分文本)复选框。如果您选择“Search text in method”(在方法中搜索文本)搜索选项,该选项将默认设置为该选项。

使用 Fast Colored TextBox 进行语法高亮

为了呈现代码搜索的结果,我需要一个能够进行语法高亮的文本编辑器。我研究了几种,并决定在我的项目中(同样在 CodeProject 上)使用 Pavel Torgashov 的优秀作品“Fast Colored TextBox”:Fast Colored TextBox for Syntax Highlighting。它确实很快!它还支持通过 Ctrl-F 在文本框中进行搜索。  

使用 KRBTabControl 进行多重搜索 

为了能够使用标签页界面启动多个搜索,我很高兴地在我的项目中(同样在 CodeProject 上)使用了 Burak299 的优秀作品“KRBTabControl”:KRBTabControl。这使我能够提供可关闭的标签页,就像浏览器标签页一样。当标签页过多无法显示时,您会在右侧看到两个小箭头,您可以用鼠标切换标签页。  

实现   

下面的代码并不完全与源代码相同,而是为了向您展示该工具工作原理的基础。

当单击“Search”按钮时,将使用选定的搜索方法(单选按钮)启动搜索。

public enum SearchType
{
    SearchTextInMethod,
    SearchCallers,
    SearchMethods,
    SearchProperties,
    SearchClasses
} 
private SearchType _searchType = new SearchType();
 
/// <summary>
/// - Do some checks to see if the input is correct and the solutions.txt file exists
/// - Update the text of the tab to the text that is being searched
/// - Show a hourglass icon on the tab during the search
/// - Start a new worker that will do the search
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSearch_Click(object sender, EventArgs e)
{
    string searchText = txtTextToSearch.Text;
    //Remove leading and trailing spaces
    searchText = searchText.Trim();

    if (searchText.Contains("(") || searchText.Contains(")"))
    {
        MessageBox.Show("Please specify searchtext without parentheses or parameters.");
        return;
    }

    if (!File.Exists(Constants.BaseDirectorySolutionsTxtPath))
    {
        MessageBox.Show("There is no solutions.txt file in the directory where the .exe resides." + 
          " Please click the [Browse] button to select a starting direcctory. Then click [Update solution List]");
    }
    else
    {
        SearchType searchType = new SearchType();

        if (!String.IsNullOrEmpty(searchText))
        {
            TabController.UpdateSearchTextOnTab(searchText);
            TabController.ShowHourGlass();

            if (rbSearchTextInMethod.Checked)
            {
                searchType = SearchType.SearchTextInMethod;
            }
            else if (rbSearchCallers.Checked)
            {
                searchType = SearchType.SearchCallers;
            }
            else if (rbSearchMethods.Checked)
            {
                searchType = SearchType.SearchMethods;
            }
            else if (rbSearchProperties.Checked)
            {
                searchType = SearchType.SearchProperties;
            }
            else if (rbSearchClasses.Checked)
            {
                searchType = SearchType.SearchClasses;
            }

            //Create and start a new worker that will do the searching for us.
            WorkerFactory.Start(searchType, searchText, txtExclude.Text, 
              txtInclude.Text, TabController.SelectedTab.Guid);
        }
        else
        {
            MessageBox.Show("Please enter text to search");
        }
    }
} 

WorkerFactory.Start 方法每次执行搜索时都会创建一个新的 Worker 对象。

public static class WorkerFactory
{
    private static List<Worker> _workerList = new List<Worker>();

    public static void Start(SearchType searchType, string searchText, 
           string filter, string include, Guid guid)
    {
        Worker worker;

        worker = new Worker(searchType, searchText, filter, include, guid);
        _workerList.Add(worker);

        worker.Start();
    }

    /// <summary>
    /// Select a worker from the workerlist with a certain Guid.
    /// </summary>
    /// <param name="guid"></param>
    /// <returns></returns>
    private static Worker SelectWorker(Guid guid)
    {
        var selectWorker = from worker in _workerList
                           where worker.Guid == guid
                           select worker;

        if (selectWorker != null && selectWorker.Count() == 1)
        {
            return (Worker)selectWorker.First();
        }

        return null;
    }

    /// <summary>
    /// If a tab is deleted the accompanying worker must be cancelled.
    /// It won't be killed, but the results will not be written to a tab anymore.
    /// If it's not needed anymore, doesn't matter because they will be cleaned up once the program quits.
    /// </summary>
    /// <param name="guid">The unique identifier of the worker</param>
    public static void Delete(Guid guid)
    {
        Worker selectWorker = SelectWorker(guid);

        //Does the worker exist in the workerlist?
        //Because, if a tab is deleted, but a worker was not started for that tab,
        //there is no worker to delete.
        if (selectWorker != null)
        {
            selectWorker.Cancel();
            _workerList.Remove(selectWorker);
        }
    }
} 

Worker 使用 BackgroundWorker 启动一个线程,该线程使用 Roslyn 启动代码搜索。

public class Worker
{
    private CodeSearcher _searcher;
    BackgroundWorker _worker;
    private string _result;
    private Guid _guid;
    private bool _cancel;
 
    public Worker(SearchType searchType, string searchText, string filter, string include, Guid guid)
    {
        _guid = guid;
        _searcher = new CodeSearcher(searchType, searchText, filter, include);

        _worker = new BackgroundWorker();
        _worker.DoWork += new DoWorkEventHandler(worker_DoWork);
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
    }

    public Guid Guid
    {
        get { return _guid; }
        set { _guid = value; }
    }

    public void Start()
    {
        _worker.RunWorkerAsync();
    }

    /// <summary>
    /// Cancel means the backgroundworker will finish it's job,
    /// but won't write the results to the tabcontroller anymore.
    /// </summary>
    public void Cancel()
    {
        _cancel = true;
    }

    private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (!_cancel)
        {
            TabController.WriteResults(_guid, _result);
        }
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
       _result = _searcher.Search();
    }
}

如果使用 Start 方法启动工作器,它会异步调用 worker_DoWork,后者调用 CodeSearcher.Search 方法,该方法根据选定的 SearchType 使用 5 种方法之一进行搜索。

public class CodeSearcher
{
    /// <summary>
    /// Search for the provided searchtext in the sourcecode files of the solutions.
    /// Use the provided SearchType (method, callers, text in method).
    /// Return the result in a string.
    /// </summary>
    /// <returns></returns>
    public string Search()
    {
      string result = "";

      List<string> excludes = CodeSearcher.GetFilters(_exclude);
      List<string> includes = CodeSearcher.GetFilters(_include);

      if (CodeRepository.Workspaces.Count() == 0)
      {
        //Get the solutions from the solutions.txt file and load them into Workspaces
        //If it doesn't exist, this will be checked at the moment user presses the [Search] button.
        CodeRepository.Solutions = CodeRepository.GetSolutions(Constants.BaseDirectorySolutionsTxtPath);

        CodeRepository.Workspaces = CodeRepository.GetWorkspaces(CodeRepository.Solutions);
      }

      if (_searchType == SearchType.SearchTextInMethod)
      {
        result = SearchMethodsForTextParallel(CodeRepository.Workspaces, _searchText, excludes, includes);
      }
      else if (_searchType == SearchType.SearchCallers)
      {
        result = SearchCallersParallel(CodeRepository.Workspaces, _searchText, excludes, includes);
      }
      else if (_searchType == SearchType.SearchMethods)
      {
        result = SearchMethodsParallel(CodeRepository.Workspaces, _searchText, excludes, includes);
      }
      else if (_searchType == SearchType.SearchProperties)
      {
        result = SearchPropertiesParallel(CodeRepository.Workspaces, _searchText, excludes, includes);
      }
      else if (_searchType == SearchType.SearchClasses)
      {
        result = SearchClassesParallel(CodeRepository.Workspaces, _searchText, excludes, includes);
      }

      return result;
    } 

如果 solutions.txt 文件存在于 RoslynCodeSearcher.exe 所在的目录中,解决方案的路径将被放入一个 List 中,并且将加载包含解决方案的 Workspaces。Workspace 是您解决方案的活动表示,它是一个项目集合,每个项目又包含一个文档集合。Workspace 提供对解决方案当前模型的访问。您可以在此处阅读更多关于它的信息:here

CodeSearcher 类中,我有五个搜索方法。搜索就在这里进行。搜索利用了 .NET 4 的 Parallel.ForEach 关键字来根据您计算机处理器的核心数量来加速。我将在这里展示其中一个搜索方法,另外 4 个可以在源代码中找到。

 /// <summary>
/// Search through the code for methods that contain the text textToSearch.
/// Return the resulting method bodies as a string.
/// excludes are used to exclude files that have paths that contain certain words.
/// includes are used to include files that have paths that contain certain words.
/// </summary>
/// <param name="workspaces"></param>
/// <param name="textToSearch"></param>
/// <param name="excludes">Projects / documents to exclude by name</param>
/// <param name="includes">Projects / documents to include by name</param>
/// <returns></returns>
public string SearchMethodsForTextParallel(List<IWorkspace> workspaces, 
  string textToSearch, List<string> excludes, List<string> includes)
{
  StringBuilder result = new StringBuilder();
  string language = "";

  foreach (IWorkspace w in workspaces)
  {
    ISolution solution = w.CurrentSolution;

    foreach (IProject project in solution.Projects)
    {
      language = project.LanguageServices.Language;

      Parallel.ForEach(project.Documents, document =>
      {
        //Filter and include document names containing certain words
        if (!excludes.Any(s => document.FilePath.ToUpper().Contains(s)) &&
            (
              includes.Count() == 0 || includes.Any(s => document.FilePath.ToUpper().Contains(s)))
            )
        {
          if (language == LANG_CS)
          {
            result.Append(SearchMethodsForTextCSharp(document, textToSearch));
          }
        }
      });
    }
  }

  return result.ToString();
}

private string SearchMethodsForTextCSharp(IDocument document, string textToSearch)
{
  StringBuilder result = new StringBuilder();

  CommonSyntaxTree syntax = document.GetSyntaxTree();
  var root = (Roslyn.Compilers.CSharp.CompilationUnitSyntax)syntax.GetRoot();

  var syntaxNodes = from methodDeclaration in root.DescendantNodes()
                   .Where(x => x is MethodDeclarationSyntax || x is PropertyDeclarationSyntax)
                    select methodDeclaration;

  if (syntaxNodes != null && syntaxNodes.Count() > 0)
  {
    foreach (MemberDeclarationSyntax method in syntaxNodes)
    {
      if (method != null)
      {
        string methodText = method.GetFullText();
        if (methodText.ToUpper().Contains(textToSearch.ToUpper()))
        {
          result.Append(GetMethodOrPropertyTextCSharp(method, document));
        }
      }
    }
  }

  return result.ToString();
}

当找到文本、调用、方法或属性时,将调用 GetMethodOrPropertyText 方法来获取包含所搜索项的方法/属性的正文。将返回方法的完整文本,包括 .cs 文件的路径。

/// <summary>
/// Get the full text of the method or property body.
/// </summary>
/// <param name="node"></param>
/// <param name="document"></param>
/// <returns></returns>
private string GetMethodOrPropertyTextCSharp(Roslyn.Compilers.CSharp.SyntaxNode node, IDocument document)
{
  StringBuilder resultStringBuilder = new StringBuilder();

  string methodText = node.GetFullText();
  bool isMethod = node is Roslyn.Compilers.CSharp.MethodDeclarationSyntax;
  string methodOrPropertyDefinition = isMethod ? "Method: " : "Property: ";

  object methodName = isMethod ? ((Roslyn.Compilers.CSharp.MethodDeclarationSyntax)node).Identifier.Value : 
    ((Roslyn.Compilers.CSharp.PropertyDeclarationSyntax)node).Identifier.Value;
  resultStringBuilder.AppendLine("//=====================================================================================");
  resultStringBuilder.AppendLine(document.FilePath);
  resultStringBuilder.AppendLine(methodOrPropertyDefinition + (string)methodName);
  resultStringBuilder.AppendLine(methodText);

  return resultStringBuilder.ToString();
} 

回到上面的 Worker 对象,当工作器完成并获得结果后,将调用 TabController.WriteResults 方法来使用结果更新 FastColoredTextBox

public static class TabController
{
  private static List<FastColoredTextBoxNS.FastColoredTextBox>
     _fastColoredTextBoxes = new List<FastColoredTextBoxNS.FastColoredTextBox>();

  /// <summary>
  /// The results of the search will be written to the tab specified with the guid
  /// </summary>
  /// <param name="guid"></param>
  /// <param name="text"></param>
  public static void WriteResults(Guid guid, string text)
  {
    //If another thread comes here, block it temporarily until this thread is finished.
    lock (_lockobj)
    {
      var selectFastColoredTextBox = from fctb in _fastColoredTextBoxes
                       where fctb.Guid == guid
                       select fctb;

      if (selectFastColoredTextBox != null && selectFastColoredTextBox.Count()==1)
      {
        FastColoredTextBox currentTextBox = (FastColoredTextBox)selectFastColoredTextBox.First();

        currentTextBox.Text = text;

        if (text == "") currentTextBox.Text = "Nothing found.";

        //move caret to start text
        currentTextBox.Selection.Start = Place.Empty;
        currentTextBox.DoCaretVisible();
      }
    }
  }
} 

如您在源代码中所见,还有许多内容比本文所示的要多。例如,可以同时从不同的标签页独立启动多个搜索。这需要一些线程和适当的锁定/处理。此外,该工具本身可以搜索 C# 和 VB.NET 源代码。

关于源代码

附加的项目将在 Visual Studio 2010 SP1(或 Visual Studio 2012,如果您下载 2012 版本)中打开并构建。在第 "How to use it" 段落中,我解释了使用该工具的先决条件。  

该项目的未来

一些关于该项目未来可能发展方向的思考:

正则表达式支持  

我希望能够使用正则表达式进行搜索,例如:查找所有名为“SaveCustomer”或“InsertCustomer”的方法。您需要输入如下的正则表达式。
(Save|Insert)Customer 

Visual Studio 扩展  

这可以重构为一个 Visual Studio 扩展。这样它就可以利用 C# 代码编辑器和其他 Visual Studio 的部分。这样它就能更强大,并且更容易被更多人使用。 

高级功能 

为了使重构跨多个解决方案的源代码更加方便,能够对源代码进行某种类型的“查询”会很好,就像 LINQ 一样。类似于 http://www.ndepend.com/Doc_CQLinq_Syntax.aspxhttps://codeproject.org.cn/Articles/408663/Using-NRefactory-for-analyzing-Csharp-code。为了使这些查询强类型而非动态,需要在某种交互式窗口中进行智能感知。也许新的 Roslyn “C# Interactive window” 可以用于此。但也许作为 Visual Studio 扩展会更容易实现。

输出  

让用户定义输出应包含的内容,例如:

  • 整个代码文件
  • 方法/类/解决方案等之间连接的图形视图。

历史    

2012-12-05

  • 为 Visual Studio 2012 和 Roslyn September 2012 CTP 创建了一个版本 
  • 修复了与 Roslyn September 2012 CTP 兼容的版本中的破坏性更改
  • 由于 Roslyn September 2012 CTP 的破坏性更改,修复了单元测试 

2012-08-21

  • 修复了搜索调用者中的一个 bug;有些调用者未被找到。

2012-08-05

  • 添加了 "precompile" 选项,可在程序启动时在内存中编译解决方案以加快速度 

2012-08-04

  • 添加了类名搜索功能和“部分文本”搜索  

2012-08-01

  • 使用了 Parallel.ForEach 进行搜索。使用 2 核时速度大约是原来的 2 倍,4 核无法测试,但可能快 4 倍。  

2012-07-28

  • 更多单元测试 (TabController)  
  • 使用 .Any() 代替 Count() > 0 
  • 单元测试以测试 @"A".ToUpper().Contains(@"B".ToUpper())@"A".IndexOf(@"B", StringComparison.OrdinalIgnoreCase) 的性能  

2012-07-24

  • 添加了单元测试   
  • 也能够搜索 VB.NET 代码  

2012-07-18

  • 添加了属性搜索功能
  • 搜索文本框的输入检查
  • 单击 [Search] 时,删除搜索文本框中的文本的开头/结尾空格
  • 根据选择的搜索类型显示“Method:”或“Property:”

2012-07-16

  • 修复了从 FastColoredTextBox 复制(Ctrl-C)的功能 
  • 线程运行时在标签页上显示沙漏图标 
  • 如果您单击“New tab”按钮,程序会自动跳转到下一个标签页
  • 标签页之间的分隔线 
  • 更改了 include / exclude 文本字段的文本,以更好地描述其含义 

2012-07-15

  • 修复了导致搜索文本中包含括号时出现错误的 bug
  • 为源代码添加了额外的注释
  • 测试了是否可以搜索不同类型的函数定义
  • 一些重构:regions 等。 
  • 为“Update solution List”按钮添加了 MessageBox。   
C# 和 VB.NET 代码搜索器 - 使用 Roslyn - CodeProject - 代码之家
© . All rights reserved.