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

Go Back 插件 for VS.NET 2003

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (14投票s)

2005 年 3 月 20 日

8分钟阅读

viewsIcon

76473

downloadIcon

664

创建一个 Visual Studio 2003 插件,以返回到之前的位置。

New context menu with GoBackAddin installed

目录

引言

在处理大型项目时,我经常需要在许多文件中跳转到不同的位置(我一直在使用“转到定义”上下文菜单命令)。一旦我沿着特定路径导航到某处,我就想回到我最初的起点。虽然我可以在离开之前使用书签来标记我的位置,但我倾向于将书签用于其他目的(而且书签只在包含源文件的上下文中工作——你无法在源文件之间使用“编辑.下一个书签”)。有人告诉我 Visual Studio 7 具有“向后导航”命令,该命令可以做到这一点,但是,我发现它有一些我不喜欢的怪癖。

此插件提供了一种更方便的“向后导航”机制。它会监视当前文档位置的导航,并提供一种返回到前一个位置或导航链中任何位置的方法。我将文档位置定义为文档(即源文件)文件名,以及该文档中文本插入光标的行号和列号。

背景

Visual Studio 通过“转到定义”和“转到引用”上下文菜单命令提供了一种快速的代码跳转机制。还有许多其他命令可以更改源文件中的当前活动编辑位置(例如,“编辑.文档结尾”、“编辑.下一个书签”、“编辑.转到下一个位置”等)。在 Visual Basic 6 中,有一个“前一个位置”上下文菜单命令可以选择,该命令可以将您直接带回到您之前的位置。不幸的是,Visual Studio 2002 和 2003(以及 2005 Beta 版)的编辑上下文菜单不包含此支持。“编辑.转到前一个位置”命令已定义,但遗憾的是,该命令用于移动到之前标记的位置(例如,来自“在文件中查找”文本搜索)。

“向后导航”命令是 VB6 中“前一个位置”命令的替代品。它有所改进,但也有几个(我不喜欢的)怪癖。具体来说:

  • 它在编辑上下文菜单中不可用(因此,要么将鼠标指针移到 VS 窗口顶部附近的标准工具栏,要么用手离开鼠标输入 'Ctrl+-'),这很不方便。
  • 每个文本插入点都会被记录下来,因此当您在源文件中单击时,导航历史记录会变得相当长(我并不觉得这很有帮助——我想要的是主要的移动,而不是所有细微的移动)。
  • 当代码导航打开一个源文件,然后您进行“向后导航”时,您需要选择该命令两次才能回到您真正的位置。这是记录每次文本插入更改的结果(当新文件打开时,文件开头会被记录为一个导航位置,即使您在视觉上看到的是其他位置)。
  • 由于代码导航而打开的文件在您从中返回时(“向后导航”)仍保持打开状态。

使用代码

加载插件后,代码窗口的上下文菜单会得到修改,添加了两个额外的命令 - 前一个位置选择位置...。这些菜单项根据您的代码导航操作启用。我的偏好是拥有一致的菜单,因此菜单项是启用/禁用的,而不是根据命令可用性出现/消失。注意:如果您未设置在启动时加载插件的选项,菜单状态将显示为已启用。当您选择其中一个命令时,插件将被加载,Visual Studio 会对每个命令执行 QueryStatus 检查,然后它们将正确显示为禁用。

No return is possible Return to previous location is possible Return to one of many previous locations is possible

不存在导航历史记录。

存在一个导航位置。

存在两个或更多导航位置。返回到最近的位置或从列表中选择。

如果 前一个位置 已启用然后被选中,则当前编辑位置将恢复到记住的位置。如果该文档是因代码导航而打开的(并且随后未被修改),则将被关闭。我的偏好是只保留我当前正在处理的文档,并且我认为自动关闭仅为“参考”检查而打开的文档是一种便利。

如果 选择位置... 已启用然后被选中,则会显示 PositionDialog 窗体(参见下图)。它显示了因导航命令而离开的文档位置历史记录。最近的位置显示在最前面。单击任何一行将导致选中并返回到该位置。

The list of document positions that you have left and can return to.

PositionDialog 还允许您清除记录的位置历史记录。“关闭跳过的文档”复选框决定了在返回时是否自动关闭沿途打开的任何文档。

关于代码

插件的骨架由 Visual Studio 插件项目向导创建(特别是 Connect 类)。有很多关于如何为 Visual Studio 编写插件的文章,所以我在这里就不赘述基础知识了。

起初,我以为我可以通过处理文本编辑器 LineChanged 事件来简单地完成。事件处理程序会收到一个指示更改性质的值(参见 MSDN 的值表)。vsTextChangedCaretMoved 值(插入点已移动)表明这会很好地工作。然而,LineChanged 事件仅在行的文本发生更改且您移开该行时触发。这可能是我没有按照希望的方式工作的原因——它会触发大量事件,并产生与“向后导航”命令相同的导航历史记录。

阅读新闻组帖子时,我发现有人也在尝试解决类似的问题,并通过为某些命令设置 BeforeExecuteAfterExecute 处理程序来解决。这就是我采用的方法,并且在大多数情况下它效果相当不错。在 OnConnection 方法中,我遍历一个编辑器命令列表,我希望在这些命令执行时收到通知。对于每个命令,我获取其相应的 EnvDTE.CommandEvents 并注册 BeforeExecuteAfterExecute 处理程序。

for( int i = 0; i < _interceptCommandNames.Length; i++ )
{
    EnvDTE.Command cmd = _vsNet.Commands.Item( _interceptCommandNames[i], -1 );
    if( null != cmd )
    {
        _commandEvents[i] = _vsNet.Events.get_CommandEvents( cmd.Guid, cmd.ID );
        _commandEvents[i].BeforeExecute += 
    new _dispCommandEvents_BeforeExecuteEventHandler( BeforeExecute );
        _commandEvents[i].AfterExecute +=
           new _dispCommandEvents_AfterExecuteEventHandler( AfterExecute );
    }
}    

最初,我只使用了 BeforeExecute 处理程序,它只是简单地记录了当前的文档位置。然而,在某些情况下,会请求代码导航,但实际并未发生导航。因此,AfterExecute 处理程序用于在缓存的位置被记录之前,验证当前文档位置是否已更改。PositionManager 类(下面代码中的 _pm 实例变量)负责所有文档位置的处理,这些位置内部使用 System.Collections.Stack 存储。它反过来依赖于 DocumentPosition 数据类,该类封装了位置信息。

private void BeforeExecute( string   Guid,
                            int      ID,
                            object   CustomIn,
                            object   CustomOut,
                            ref bool CancelDefault)
{
    // Don't indicate that we've handled this command event.
    CancelDefault = false;

    try
    {
        _pm.CacheCurrentPosition();
    }
    catch( Exception ex )
    {
        DisplayInOutputWindow( ex.ToString() );
    }
}


private void AfterExecute(  string   Guid,
                            int      ID,
                            object   CustomIn,
                            object   CustomOut )
{
    try
    {
        if( _pm.PositionHasChanged )
        {
            _pm.RecordCachedPosition();
        }
        else
        {
            _pm.ClearCachedPosition();
        }
    }
    catch( Exception ex )
    {
        DisplayInOutputWindow( ex.ToString() );
    }
}

PositionManager.RecordCachedPosition 方法执行检查,以确定当前文档位置是否需要打开文档。如果需要,它会设置 OpenedDocumentName 属性,以便在返回时关闭该文档。

public void CacheCurrentPosition()
{
    _position = GetCurrentPosition();
    _openedDocumentCount = _vsNet.Documents.Count;
}

public void RecordCachedPosition()
{
    //Since this method should be called after some code navigation
    //    event, any change in the document count means that a new
    //    document was opened as a result of the navigation.  That means
    //    its not a working document and can be closed (if its not later
    //    modified) when returning back to the original location.
    if( _openedDocumentCount != _vsNet.Documents.Count )
    {
        DocumentPosition newPosition = GetCurrentPosition();
        _position.OpenedDocumentName = newPosition.DocumentName;
    }
    _positionStack.Push( _position );
}

选择 前一个位置选择位置... 命令之一会导致调用 Connect.Exec 方法,该方法必须决定实际正在处理哪个命令,然后采取相应行动。下面显示了该方法的相关代码。

bool closeOnReturn = true;
DocumentPosition position = null;
if( COMMAND_PREVIOUS_LOCATION == commandName )
{
    position = _pm.PreviousPosition;
}
if( COMMAND_CHOOSE_LOCATION == commandName )
{
    PositionDialog pd = new PositionDialog();
    pd.InitializeForm( _pm );
    if( DialogResult.OK == pd.ShowDialog() )
    {
        position = pd.SelectedPosition;
        closeOnReturn = pd.CloseSkippedDocuments;
    }
}

if( null != position )
{
    MoveToPosition( position, closeOnReturn );
}
handled = true;

Connect.MoveToPosition 方法非常直接。它负责在返回时关闭文档。唯一需要注意的是调用 MoveToDisplayColumn 并传入行号和列号。这会将光标放置在正确的位置(否则,列号将被视为从行开头开始的字符数)。

private void MoveToPosition( DocumentPosition position, bool closeDocument )
{
    if( closeDocument && ( 0 < position.OpenedDocumentName.Length ) )
    {
        Document leavingDoc = _vsNet.Documents.Item( position.OpenedDocumentName );
        if( ( null != leavingDoc ) && leavingDoc.Saved )
        {
            leavingDoc.Close( EnvDTE.vsSaveChanges.vsSaveChangesPrompt );
        }
    }

    //Move to the specified position.  First activate the document, and
    //    then move to the original line number and column number.
    Document doc = _vsNet.Documents.Item( position.DocumentName );
    if( null != doc )
    {
        doc.Activate();
        TextSelection ts = (TextSelection)_vsNet.ActiveDocument.Selection;
        ts.MoveToDisplayColumn( position.LineNumber,
                                position.ColumnNumber,
                                false );
    }
}

限制

Visual Studio 中存在一些代码导航机制,它们的关联事件目前未被捕获。如果您使用其中一种机制导航离开了当前的编辑位置,则没有拦截事件机制可以在移动到所需位置之前捕获当前位置(因此此插件将无法将您带回到起始位置)。这可能导致一种误导情况,即此插件的菜单项已被之前的代码导航事件启用,而选择 前一个位置 会将您带回到一个更早的文档位置,而不是您以为要回去的位置。

具体来说,目前未捕获的导航事件是:

  • 单击“查找”对话框的“查找下一个”按钮(尽管 Edit.FindNext 命令事件已被捕获,但该命令似乎并未通过按钮单击来执行)。
  • 在成员定义组合框中选择一个名称。
  • 单击查找窗口结果或任务列表项。

学到的教训

我发现开发 VS.NET 插件用户界面非常不方便(特别是工具栏命令——对话框窗体与常规 WinForm 应用程序没有区别)。插件的用户界面控件仅创建一次(在插件设置期间),我无法为其设置断点。创建后,就没有方便的机制来删除这些工具栏按钮。我最终不得不这样做:

  • 使用插件管理器卸载插件并退出 Visual Studio。
  • 删除插件 DLL 文件。
  • 重新启动 Visual Studio,然后在插件安装的(每个)命令按钮上单击。Visual Studio 会抱怨插件无法正常工作,并询问您是否要删除该插件(单击“确定”)。
  • 重新创建插件的注册表项和值。(此插件的注册表项为 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns\GoBackAddin.Connect),然后重新运行 Visual Studio 创建的 ReCreateCommands.reg 文件。
  • 重新启动 Visual Studio 并通过插件管理器加载插件。
  • 当以上方法不起作用时,请在命令窗口中使用命令 devenv /setup。

修订历史

  • 2005-03-20:

    已根据 mav.northwind 的反馈修改了介绍性文本。

  • 2005-03-19:

    初始版本。

© . All rights reserved.