程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Visual Studio 2008可擴展性開發(七):操作文本編輯器

Visual Studio 2008可擴展性開發(七):操作文本編輯器

編輯:關於.NET

前言

在可擴展性開發(六)中,我介紹了對於Solution Explorer的基本操作。不過,對咱 們開發人員來說,絕大部分時間面對的還是編輯器。

VS2008的編輯器功能已經頗為強大了,如果我們能熟練使用快捷鍵,編寫代碼的過程 是相當舒服的,就像《卓有成效的程序員》中所說:

“編程時始終優先使用鍵盤而非鼠標”

但問題在於,VS面向的是所有的開發人員群體,它只能夠提供最通用的功能,如果對 VS的編輯器有些額外的需求,我們只好自己動手了,本文將介紹如何擴展文本編輯器。

AOM中編輯器相關的接口

跟以前一樣,這裡首先簡單介紹一下AOM中的相關接口、類型。

1)Documents

在默認情況下,VS會以標簽式文檔呈現打開的各個文檔。這些文檔的集合在AOM中就是 Documents,它實現了IEnumerable接口。通過該接口,我們可以獲取當前打開的文檔,它 的重要屬性和方法有:

Count:打開文檔的數目;

Add():向集合中添加新的文檔;

CloseAll():關閉所有文檔,它的參數為vsSaveChanges枚舉,可提供關閉時的行為選 項,比如提示用戶進行保存;

Item():根據索引獲取集合中的某個文檔;

Open():打開一個文檔;

SaveAll():保存所有文檔。

這些成員的含義是相當簡單、直白的。我們可以通過循環變量所有打開的文檔,以獲 取所有文檔的信息,對於單個文檔來說,它對應於Document接口。

2)Document

表示在VS中打開進行編輯的文檔。它的成員較多,這裡僅介紹一下比較重要的幾個:

FullName/Path/Name:文檔的全名、所在目錄、文件名;

Language:文檔的語言類型,如CSharp;

ProjectItem:獲取與文檔關聯的ProjectItem對象;

Selection:文檔中的選定內容;

Type:文檔的類型;

Activate():將焦點移至該文檔;

Close():關閉文檔;

Redo()/Undo():執行Redo/Undo操作;

Save():保存文檔。

關於Document成員的詳細信息,請參看這裡。其中的Selection屬性非常有用,因為很 多時候我們都是先選中文檔的部分內容,再進行相應的操作。另外,在打開的多個文檔中 ,只有一個處於活動狀態,可以使用DTE.ActiveDocument屬性來快速獲取該文檔。

在獲取文檔的引用後,下一步就可以考慮如何進行編輯了。我們得了解5個接口: TextSelection、TextPoint、EditPoint、VirtualPoint、TextDocument。相信在了解了 這些接口後,你在操作編輯器時會得心應手的。

3)TextSelection

該接口提供對文檔的編輯操作和選定文本的訪問。它的成員比Document還有多很多, 功能非常全面,應當可以滿足絕大部分需要了,這裡就不再一一列舉了,可以參看MSDN的 內容。

我們在手工輸入代碼時,可以看作總是在光標處輸入,也可以把光標看作一個點,這 個點包含一些信息,如行號、列號等,這樣VS就可以處理輸入的內容,在Add-In中以編程 方式輸入時與此類似,這個“點”就是TextPoint。

4)TextPoint

該接口表示文檔中的某個位置,EditPoint和VirtualPoint繼承於此。它的主要屬性和 方法有:

AbsoluteCharOffset:從文檔開始計算的絕對字符位置,從1開始;

AtEndOfDocument/AtEndOfLine:指示該點是否處於文檔/行的結尾;

AtStartOfDocument/AtStartOfLine:指示該點是否處於文檔/行的開頭;

DisplayColumn:顯示列號;

Line:行號;

LineCharOffset:該點在行內的位置;

LineLength:該點所在行的字符數;

CreateEditPoint():創建一個EditPoint對象以對文檔進行編輯;

EqualTo()/GreaterThan()/LessThan():與另一個TextPoint比較相互的位置關系;

關於TextPoint的所有成員信息,請參看這裡。光有TextPoint還不能編輯,要真正進 行編輯,得使用EditPoint接口。

5)EditPoint

EditPoint從TextPoint那裡繼承了所有的屬性和方法,它還提供了很多用於編輯的屬 性和方法,比如常見的插入、刪除、剪切、粘帖、書簽操作,還有位置的移動等等,使我 們在編輯文本時擁有了強大的能力。關於EditPoint的所有成員信息,請參看這裡。

有時一行內的字符數很多,此時在屏幕能就看不到了,也就是說超出了文檔的右邊距 ,要操作在右邊距之外的文本需要VirtualPoint。

6)VirtualPoint

VirtualPoint也繼承自TextPoint,只是添加了少數幾個屬性和方法,這裡就不再贅述 了,可以參看這裡。

7)TextDocument

最後一個接口是TextDocument,它表示在編輯器中打開的文檔。在你了解了前面幾個 接口的成員後,對TextDocument的成員也很容易了解了。

在操作文本時,大部分時候可以選擇從TextSelection開始,不過在某些情況下 TextDocument是個不錯的開始,可以考慮先使用TextDocument,如果不能滿足需要,再轉 向前面的幾個接口。

在介紹了這麼多接口之後,該看一個例子了。

CodeTemplate示例

0)問題分析

這一次要給NEnhancer添加的功能是代碼模板。它源自我當前的項目需要,項目要求每 次修改代碼都要添加這樣的注釋:

C# Code - 代碼中的一種注釋

//-------------Change Begin------------------------------------
//-----------Code change log for Item 1001  ------------------------------ -
//-----------Modified by : Anders Cui     Change Date: 03/30/2009
//-----------Changes Begin------------------------------------------------- -----
Console.WriteLine("Hello, World");
//-----------Changes End for Item 1001 ----------------------------------

雖然我不喜歡,但是沒辦法,還是要一次次地添加注釋。一旦在重復地做著什麼事情 ,我就想有什麼更好的辦法可以替代。

先來分析下這段注釋。由於是維護項目,每次改動都對應著客戶發過來的一個需求項 ,Item 1001表示需求項的Id,中間還有代碼編寫者和日期,另外還要把選中代碼包含起 來。起初我考慮使用Code Snippet,但有兩個不方便的地方:一是對於新的需求,都得更 新CodeSnippet;二是不能自動生成日期。

這兩個地方在Add-In中都不是問題。可以建立這樣的模板:

C# Code - 注釋的簡單模板

//-------------Change Begin------------------------------------
//-----------Code change log for $item$  -------------------------------
//-----------Modified by : $author$     Change Date: $today$
//-----------Changes Begin------------------------------------------------- -----
$selected$
//-----------Changes End for $item$ ----------------------------------

我們把一些變化的文本提取出來,即$item$、$author$,作為參數進行配置,供當前 這個模板使用;而對於日期,可將$today$作為“內置”或“全局”的參數,每個模板都 可以使用。在使用Add-In插入代碼模板時,只要將各個參數的信息替換到模板中,然後插 入到文檔中即可。根據這樣的思路,可以建立如下的模板文件:

XML Code - 代碼模板的配置文件

<?xml version="1.0" encoding="utf-8" ?>
<templates>
    <builtInParams>
        <param format="MM/dd/yyyy">today</param>
        <param format="yyyy-MM-dd">now</param>
        <param>guid</param>
        <param>solution</param>
        <param>project</param>
        <param>file</param>
    </builtInParams>
    <template name="Comment">
        <content>
        <![CDATA[
        //-------------Change Begin--------------------------------- ---
        //-----------Code change log for $item$  ------------------ -------------
        //-----------Modified by : $author$     Change Date: $today$
        //-----------Changes Begin---------------------------------- --------------------
        $selected$
        //-----------Changes End for $item$ ------------------------ ----------
        ]]>
        </content>
        <params>
            <param name="item">1001</param>
            <param name="author">Anders Cui</param>
        </params>
    </template>
    <template name="Codes">
        <content>
            <![CDATA[
        //-------------Change Begin--------------------------------- ---
        //-----------Code change log for $item$  ------------------ -------------
        //-----------Modified by : $author$     Change Date: $now$
        //-----------Changes Begin---------------------------------- --------------------
        $selected$
        //-----------Changes End for $item$ ------------------------ ----------
        ]]>
        </content>
        <params>
            <param name="item">1002</param>
            <param name="author">Anders Liu</param>
        </params>
    </template>
</templates>

在節點builtInParams下,可以定義多個“全局”參數,應用於多個模板,對於日期類 型的參數來說,可以指定格式;每個template節點定義了一個模板,模板可以有自己的參 數;$selected$參數比較特別,其作用是指示選中文件所放的位置;最後,約定所有的參 數都放在兩個“$”中間。

下面來看看如何實現上面的思路。

1)添加命令

在添加命令前,看看NEnhancer當前的代碼,裡面有很多代碼是比較通用的,每次開發 Add-In都可以使用,所以把它提取出來放到DTEHelper類中。

在以前添加命令時,往往使用這樣的代碼:

C# Code - 添加命令的第一種方式

// Add command
Command command = commands.AddNamedCommand2(addin, cmdName, buttonText, toolTip,
    useMsoButton, iconIndex, ref contextGUIDS,
    (int)vsCommandStatus.vsCommandStatusSupported + (int) vsCommandStatus.vsCommandStatusEnabled,
    (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
if (command != null && cmdBar != null)
{
    command.AddControl(cmdBar, position);
}

然後要在QueryStatus和Exec方法中添加代碼來實現該命令,這樣可以每次添加一個菜 單項。這對於當前的例子就不太合適了,因為模板可以是多個,即使根據配置文件動態添 加這些菜單項,如果模板多了,菜單就會變得很長。一個解決方案是添加嵌套菜單,將所 有這些模板對應的菜單放在一個子菜單下。這裡介紹添加菜單的另一種方式:

C# Code - 添加彈出菜單和菜單項

// 向命令欄添加一個彈出菜單
int templatePopupIndex = codeWinCommandBar.Controls.Count + 1;
CommandBarPopup codeTemplatePopup = codeWinCommandBar.Controls.Add(
    MsoControlType.msoControlPopup, Type.Missing, Type.Missing,
    templatePopupIndex, true) as CommandBarPopup;
codeTemplatePopup.Caption = "Code Template";

// 向彈出菜單添加菜單項
CommandBarButton codeTemplateCmd = helper.AddButtonToPopup(codeTemplatePopup,
    codeTemplatePopup.Controls.Count + 1, "Code Template 1", "Code Template 1");
codeTemplateCmdEvent =
    _applicationObject.Events.get_CommandBarEvents(codeTemplateCmd) as CommandBarEvents;
codeTemplateCmdEvent.Click +=
    new _dispCommandBarControlEvents_ClickEventHandler (CodeTemplateCmdEvent_Click);

添加一個普通的菜單項,也就是添加一個CommandBarButton類型的控件,以這種方式 添加菜單項時,我們又看到熟悉的Event.Click += ...了。

在CodeTemplate例子中,我們可以在Add-In運行時讀取配置文件,根據模板的個數生 成相應個數的菜單項。界面看起來差不多是這樣的:

菜單項的標題是配置文件中模板的名字,並且所有這些菜單項使用同一個處理函數:

C# Code - 命令的事件處理函數

private void codeTemplateCmdEvent_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
{
    CommandBarControl ctrl = CommandBarControl as CommandBarControl;
    string content = CodeTemplateManager.Instance.GetTemplateContent (ctrl.Caption);

    TextSelection selected = _applicationObject.ActiveDocument.Selection as TextSelection;
    selected.Text = content.Replace (CodeTemplateManager.Instance.SELECTED_PARAM, selected.Text);
}

CodeTemplateManager是用於處理代碼模板邏輯的類,通過其GetTemplateContent方法 可以當前菜單項對應的模板內容。接著通過TextSelection獲取活動文檔的選定文本,將 這些文本替換為模板的內容。(CodeTemplateManager的代碼可以在文章結尾下載的代碼 中看到)

SELECTED_PARAM也就是前面提到的$selected$參數,事實上,到這裡除此參數之外的 參數都已置換完畢,所以可將當前選中的文本放入作為代碼模板的最終內容。這樣做是可 以將代碼插入了,不過速度有些慢,我也沒找到原因,不過倒可以借此機會換一種方式來 演示其它API的使用:

C# Code - 使用EditPoint編輯文檔

private void codeTemplateCmdEvent_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
{
    CommandBarControl ctrl = CommandBarControl as CommandBarControl;
    string content = CodeTemplateManager.Instance.GetTemplateContent (ctrl.Caption);

    int indexOfSelectedParam = CodeTemplateManager.Instance.IndexOfSelectedParam(content);
    bool surroundSelectedText = (indexOfSelectedParam >= 0);

    TextSelection selected = _applicationObject.ActiveDocument.Selection as TextSelection;
    EditPoint topPoint = selected.TopPoint.CreateEditPoint();
    EditPoint bottomPoint = selected.BottomPoint.CreateEditPoint ();

    if (surroundSelectedText)
    {
        string beforeSelectedParam =
            CodeTemplateManager.Instance.GetTextBeforeSelectedParam(content);
        string afterSelectedParam =
            CodeTemplateManager.Instance.GetTextAfterSelectedParam(content);

        topPoint.LineUp(1);
        topPoint.EndOfLine();
        topPoint.Insert(Environment.NewLine);
        topPoint.Insert(beforeSelectedParam);

        bottomPoint.EndOfLine();
        bottomPoint.Insert(Environment.NewLine);
        bottomPoint.Insert(afterSelectedParam);
    }
    else
    {
        topPoint.Delete(bottomPoint);
        topPoint.Insert(content);
    }
}

這裡的做法,不是將模板內容生成後整體替換選定文本,而是將模板內容分為三部分 :選定文本之前的文本、選定文本本身和選定文本之後的文本,這樣我們只要在選定文本 的前面和後面分別寫入文本即可。如果模板不包含$selected$,這裡就認為將使用模板內 容替換選定文本,其做法是先刪除選定文本,在寫入模板內容,這樣處理後,速度快了很 多。

現在這個功能在我所在的小團隊已經開始使用了,已經節省了不少

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved