VS 扩展:TextPoint.GreaterThan / LessThan 对于大文件非常慢
VS Extension: TextPoint.GreaterThan / LessThan very slow for large files
我正在开发一个 VS 扩展,它需要知道 text-cursor 当前位于哪个 class 成员(方法、属性等)。它还需要了解 parents(例如 class、嵌套的 classes 等)。它需要知道成员的类型、名称和行号或 class。当我说 "Type" 时,我的意思是 "method" 或 "property" 不一定是“.NET 类型”。
目前我在这里使用这段代码:
public static class CodeElementHelper
{
public static CodeElement[] GetCodeElementAtCursor(DTE2 dte)
{
try
{
var cursorTextPoint = GetCursorTextPoint(dte);
if (cursorTextPoint != null)
{
var activeDocument = dte.ActiveDocument;
var projectItem = activeDocument.ProjectItem;
var codeElements = projectItem.FileCodeModel.CodeElements;
return GetCodeElementAtTextPoint(codeElements, cursorTextPoint).ToArray();
}
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return null;
}
private static TextPoint GetCursorTextPoint(DTE2 dte)
{
var cursorTextPoint = default(TextPoint);
try
{
var objTextDocument = (TextDocument)dte.ActiveDocument.Object();
cursorTextPoint = objTextDocument.Selection.ActivePoint;
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return cursorTextPoint;
}
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
var returnValue = new List<CodeElement>();
if (codeElements == null)
return null;
int count = 0;
foreach (CodeElement element in codeElements)
{
if (element.StartPoint.GreaterThan(objTextPoint))
{
// The code element starts beyond the point
}
else if (element.EndPoint.LessThan(objTextPoint))
{
// The code element ends before the point
}
else
{
if (element.Kind == vsCMElement.vsCMElementClass ||
element.Kind == vsCMElement.vsCMElementProperty ||
element.Kind == vsCMElement.vsCMElementPropertySetStmt ||
element.Kind == vsCMElement.vsCMElementFunction)
{
returnValue.Add(element);
}
var memberElements = GetCodeElementMembers(element);
var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint);
if (objMemberCodeElement != null)
{
returnValue.AddRange(objMemberCodeElement);
}
break;
}
}
return returnValue;
}
private static CodeElements GetCodeElementMembers(CodeElement codeElement)
{
CodeElements codeElements = null;
if (codeElement is CodeNamespace)
{
codeElements = (codeElement as CodeNamespace).Members;
}
else if (codeElement is CodeType)
{
codeElements = (codeElement as CodeType).Members;
}
else if (codeElement is CodeFunction)
{
codeElements = (codeElement as CodeFunction).Parameters;
}
return codeElements;
}
}
所以这目前有效,如果我调用 GetCodeElementAtCursor,我将获得该成员,并且 parents 返回。 (这是有点旧的代码,但我相信我最初是从 Carlos' blog 中获取它并从 VB 移植它的)。
我的问题是,当我的扩展用于非常大的代码时,例如 auto-generated 文件有几千行,它会使 VS 陷入困境。几乎无法使用。 运行 分析器显示热线是
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
foreach (CodeElement element in codeElements)
{
...
/*-->*/ if (element.StartPoint.GreaterThan(objTextPoint)) // HERE <---
{
// The code element starts beyond the point
}
/*-->*/ else if (element.EndPoint.LessThan(objTextPoint)) // HERE <----
{
// The code element ends before the point
}
else
{
...
var memberElements = GetCodeElementMembers(element);
/*-->*/ var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint); // AND, HERE <---
...
}
}
return returnValue;
}
所以第三个很明显,它是对自身的递归调用,因此影响它的任何因素都会影响对自身的调用。但是,前两个,我不确定如何修复。
- 是否有其他方法可以用来检索我的光标所在的成员类型(class、方法、属性等)、名称、行号和 parents ?
- 我可以做些什么来让
TextPoint.GreaterThan
和 TestPoint.LessThan
方法表现得更好吗?
- 或者,我S.O.L.?
不管是什么方法,只需要支持VS2015或更新版本即可
谢谢!
更新:回答 Sergey 的评论 - 它确实似乎是由 .GreaterThan
/ .LessThan()
引起的。我已经分离了代码并且 slow-down 肯定发生在那些方法调用上,而不是 element.StartPoint
和 element.EndPoint
.
的 属性 访问器
使用 GetCursorTextPoint 获取 TextPoint 后,您可以使用 TextPoint.CodeElement 属性 查找当前代码元素:
EnvDTE.TextPoint p = GetCursorTextPoint(DTE);
foreach (EnvDTE.vsCMElement i in Enum.GetValues(typeof(EnvDTE.vsCMElement)))
{
EnvDTE.CodeElement e = p.CodeElement[i];
if (e != null)
System.Windows.MessageBox.Show(i.ToString() + " " + e.FullName);
}
我最终选择了使用一些新的 roslyn 东西。下面的代码(几乎)与我上面的问题中的代码完全相同,除了返回一个 Moniker。
我将此标记为答案,但由于 Sergey 对他的回答非常有帮助,而且我的 Roslyn 代码的灵感实际上是 ,这也是他的答案,他绝对值得得分:).
代码
public static (string, ImageMoniker)[] GetSyntaxHierarchyAtCaret(IWpfTextView textView)
{
var caretPosition =
textView.Caret.Position.BufferPosition;
var document =
caretPosition.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
var syntaxRoot =
document.GetSyntaxRootAsync().Result;
var caretParent =
syntaxRoot.FindToken(caretPosition).Parent;
var returnValue = new List<(string, ImageMoniker)>();
while (caretParent != null)
{
var kind = caretParent.Kind();
switch (kind)
{
case SyntaxKind.ClassDeclaration:
{
var dec = caretParent as ClassDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Class));
break;
}
case SyntaxKind.MethodDeclaration:
{
var dec = caretParent as MethodDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Method));
break;
}
case SyntaxKind.PropertyDeclaration:
{
var dec = caretParent as PropertyDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(), KnownMonikers.Property));
break;
}
}
caretParent = caretParent.Parent;
}
return returnValue.ToArray();
}
依赖关系
由于我要返回一个元组,您将需要 System.ValueTuple and the Roslyn stuff requires Microsoft.CodeAnalysis.EditorFeatures.Text, Microsoft.CodeAnalysis.CSharp,加上所有依赖项。
目标版本 VS2015/2017 和所需的 .NET 版本
CodeAnalysis 程序集要求您以(我认为).NET 4.6.1 或更高版本为目标。 CodeAnalysis 程序集的版本也直接关系到它可以支持哪个版本的 VS。我还没有看到关于此的任何官方文档(我认为应该在每个 msdn 页面的顶部以粗体红色字母发布关于此的内容!)但是 包含用于不同 VS 目标的版本。您最早可以定位的似乎是 VS2015 (RTM)。我个人使用的是v1.3.2,应该支持VS2015 Update 3或以上版本。
性能
我没有通过分析器 运行 这样做,但它 运行 相当流畅。一开始有几秒钟,在大文件上,它不起作用(我假设文件正在被索引) - 但如果你仔细观察,VS 中的很多功能在索引之前不起作用(或不管它是什么)是完整的。你几乎不会注意到它。在一个小文件上,它是微不足道的。
(与问题稍微无关,但可能对某人有帮助...)
给任何使用 CaretChanged 事件驱动这样的功能,运行遇到性能问题的人的一个提示:我建议使用调度程序并限制调用次数。下面的代码将向调用添加 200 毫秒的延迟,并且每 200 毫秒不允许超过一个调用。好吧,至少 200 毫秒。它是不可预测的,但它会 运行 当它能够 - 低优先级 (DispatcherPriority.ApplicationIdle):
private readonly IWpfTextView _textView;
private readonly DispatcherTimer _throttleCursorMove;
...
// constructor
{
_textView.Caret.PositionChanged += HandleCaretPositionChanged;
_throttleCursorMove = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
_throttleCursorMove.Tick += (sender, args) => CaretPositionChanged();
_throttleCursorMove.Interval = new TimeSpan(0, 0, 0, 0, 200);
}
private void HandleCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
{
if (!_throttleCursorMove.IsEnabled)
_throttleCursorMove.Start();
}
private void CaretPositionChanged()
{
_throttleCursorMove.Stop();
...
var hierarchy = CodeHierarchyHelper.GetSyntaxHierarchyAtCaret(_textView);
...
}
...
我正在开发一个 VS 扩展,它需要知道 text-cursor 当前位于哪个 class 成员(方法、属性等)。它还需要了解 parents(例如 class、嵌套的 classes 等)。它需要知道成员的类型、名称和行号或 class。当我说 "Type" 时,我的意思是 "method" 或 "property" 不一定是“.NET 类型”。
目前我在这里使用这段代码:
public static class CodeElementHelper
{
public static CodeElement[] GetCodeElementAtCursor(DTE2 dte)
{
try
{
var cursorTextPoint = GetCursorTextPoint(dte);
if (cursorTextPoint != null)
{
var activeDocument = dte.ActiveDocument;
var projectItem = activeDocument.ProjectItem;
var codeElements = projectItem.FileCodeModel.CodeElements;
return GetCodeElementAtTextPoint(codeElements, cursorTextPoint).ToArray();
}
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return null;
}
private static TextPoint GetCursorTextPoint(DTE2 dte)
{
var cursorTextPoint = default(TextPoint);
try
{
var objTextDocument = (TextDocument)dte.ActiveDocument.Object();
cursorTextPoint = objTextDocument.Selection.ActivePoint;
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return cursorTextPoint;
}
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
var returnValue = new List<CodeElement>();
if (codeElements == null)
return null;
int count = 0;
foreach (CodeElement element in codeElements)
{
if (element.StartPoint.GreaterThan(objTextPoint))
{
// The code element starts beyond the point
}
else if (element.EndPoint.LessThan(objTextPoint))
{
// The code element ends before the point
}
else
{
if (element.Kind == vsCMElement.vsCMElementClass ||
element.Kind == vsCMElement.vsCMElementProperty ||
element.Kind == vsCMElement.vsCMElementPropertySetStmt ||
element.Kind == vsCMElement.vsCMElementFunction)
{
returnValue.Add(element);
}
var memberElements = GetCodeElementMembers(element);
var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint);
if (objMemberCodeElement != null)
{
returnValue.AddRange(objMemberCodeElement);
}
break;
}
}
return returnValue;
}
private static CodeElements GetCodeElementMembers(CodeElement codeElement)
{
CodeElements codeElements = null;
if (codeElement is CodeNamespace)
{
codeElements = (codeElement as CodeNamespace).Members;
}
else if (codeElement is CodeType)
{
codeElements = (codeElement as CodeType).Members;
}
else if (codeElement is CodeFunction)
{
codeElements = (codeElement as CodeFunction).Parameters;
}
return codeElements;
}
}
所以这目前有效,如果我调用 GetCodeElementAtCursor,我将获得该成员,并且 parents 返回。 (这是有点旧的代码,但我相信我最初是从 Carlos' blog 中获取它并从 VB 移植它的)。
我的问题是,当我的扩展用于非常大的代码时,例如 auto-generated 文件有几千行,它会使 VS 陷入困境。几乎无法使用。 运行 分析器显示热线是
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
foreach (CodeElement element in codeElements)
{
...
/*-->*/ if (element.StartPoint.GreaterThan(objTextPoint)) // HERE <---
{
// The code element starts beyond the point
}
/*-->*/ else if (element.EndPoint.LessThan(objTextPoint)) // HERE <----
{
// The code element ends before the point
}
else
{
...
var memberElements = GetCodeElementMembers(element);
/*-->*/ var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint); // AND, HERE <---
...
}
}
return returnValue;
}
所以第三个很明显,它是对自身的递归调用,因此影响它的任何因素都会影响对自身的调用。但是,前两个,我不确定如何修复。
- 是否有其他方法可以用来检索我的光标所在的成员类型(class、方法、属性等)、名称、行号和 parents ?
- 我可以做些什么来让
TextPoint.GreaterThan
和TestPoint.LessThan
方法表现得更好吗? - 或者,我S.O.L.?
不管是什么方法,只需要支持VS2015或更新版本即可
谢谢!
更新:回答 Sergey 的评论 - 它确实似乎是由 .GreaterThan
/ .LessThan()
引起的。我已经分离了代码并且 slow-down 肯定发生在那些方法调用上,而不是 element.StartPoint
和 element.EndPoint
.
使用 GetCursorTextPoint 获取 TextPoint 后,您可以使用 TextPoint.CodeElement 属性 查找当前代码元素:
EnvDTE.TextPoint p = GetCursorTextPoint(DTE);
foreach (EnvDTE.vsCMElement i in Enum.GetValues(typeof(EnvDTE.vsCMElement)))
{
EnvDTE.CodeElement e = p.CodeElement[i];
if (e != null)
System.Windows.MessageBox.Show(i.ToString() + " " + e.FullName);
}
我最终选择了使用一些新的 roslyn 东西。下面的代码(几乎)与我上面的问题中的代码完全相同,除了返回一个 Moniker。
我将此标记为答案,但由于 Sergey 对他的回答非常有帮助,而且我的 Roslyn 代码的灵感实际上是
代码
public static (string, ImageMoniker)[] GetSyntaxHierarchyAtCaret(IWpfTextView textView)
{
var caretPosition =
textView.Caret.Position.BufferPosition;
var document =
caretPosition.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
var syntaxRoot =
document.GetSyntaxRootAsync().Result;
var caretParent =
syntaxRoot.FindToken(caretPosition).Parent;
var returnValue = new List<(string, ImageMoniker)>();
while (caretParent != null)
{
var kind = caretParent.Kind();
switch (kind)
{
case SyntaxKind.ClassDeclaration:
{
var dec = caretParent as ClassDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Class));
break;
}
case SyntaxKind.MethodDeclaration:
{
var dec = caretParent as MethodDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Method));
break;
}
case SyntaxKind.PropertyDeclaration:
{
var dec = caretParent as PropertyDeclarationSyntax;
returnValue.Add((dec.Identifier.ToString(), KnownMonikers.Property));
break;
}
}
caretParent = caretParent.Parent;
}
return returnValue.ToArray();
}
依赖关系
由于我要返回一个元组,您将需要 System.ValueTuple and the Roslyn stuff requires Microsoft.CodeAnalysis.EditorFeatures.Text, Microsoft.CodeAnalysis.CSharp,加上所有依赖项。
目标版本 VS2015/2017 和所需的 .NET 版本
CodeAnalysis 程序集要求您以(我认为).NET 4.6.1 或更高版本为目标。 CodeAnalysis 程序集的版本也直接关系到它可以支持哪个版本的 VS。我还没有看到关于此的任何官方文档(我认为应该在每个 msdn 页面的顶部以粗体红色字母发布关于此的内容!)但是
性能
我没有通过分析器 运行 这样做,但它 运行 相当流畅。一开始有几秒钟,在大文件上,它不起作用(我假设文件正在被索引) - 但如果你仔细观察,VS 中的很多功能在索引之前不起作用(或不管它是什么)是完整的。你几乎不会注意到它。在一个小文件上,它是微不足道的。
(与问题稍微无关,但可能对某人有帮助...)
给任何使用 CaretChanged 事件驱动这样的功能,运行遇到性能问题的人的一个提示:我建议使用调度程序并限制调用次数。下面的代码将向调用添加 200 毫秒的延迟,并且每 200 毫秒不允许超过一个调用。好吧,至少 200 毫秒。它是不可预测的,但它会 运行 当它能够 - 低优先级 (DispatcherPriority.ApplicationIdle):
private readonly IWpfTextView _textView;
private readonly DispatcherTimer _throttleCursorMove;
...
// constructor
{
_textView.Caret.PositionChanged += HandleCaretPositionChanged;
_throttleCursorMove = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
_throttleCursorMove.Tick += (sender, args) => CaretPositionChanged();
_throttleCursorMove.Interval = new TimeSpan(0, 0, 0, 0, 200);
}
private void HandleCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
{
if (!_throttleCursorMove.IsEnabled)
_throttleCursorMove.Start();
}
private void CaretPositionChanged()
{
_throttleCursorMove.Stop();
...
var hierarchy = CodeHierarchyHelper.GetSyntaxHierarchyAtCaret(_textView);
...
}
...