Outlining (Managed Package Framework)
Source code can become quite complex and therefore getting an overview of the program becomes necessary. By supporting outlining, it is possible to collapse a complex program into an overview or outline. For example, in C#, all methods can be collapsed to a single line, showing only the method signature. In addition, structures and classes can be collapsed to show only the names of the structures and classes. Inside a single method, complex logic can be collapsed to show the overall flow by showing only the first line of statements such as foreach, if, and while.
Enabling Support for Outlining
The AutoOutlining registry entry is set to 1 to enable automatic outlining. Automatic outlining sets up a parse of the whole source when a file is loaded or changed in order to identify hidden regions and show the outlining glyphs. Outlining can also be controlled manually by the user.
The value of the AutoOutlining registry entry can be obtained through the AutoOutlining property on the LanguagePreferences class. The AutoOutlining registry entry can be initialized with a named parameter to the ProvideLanguageServiceAttribute attribute (see Registering a Language Service (Managed Package Framework) for details).
The Hidden Region
To provide outlining, your language service must support what are called hidden regions. These are spans of code that can be made visible or hidden. A language can also support customizable hidden regions. For example, C# provides the #region/#endregion pair that wraps a hidden region.
Hidden regions are managed by a hidden region manager as represented by the IVsHiddenTextSession interface, which Visual Studio supplies if hidden regions are supported.
Outlining uses hidden regions which are represented by objects that implement the IVsHiddenRegion interface and contain the span of the hidden region, the current visible state, and the text to be shown (the ToolTip) when the user holds the cursor over the hidden region after it is collapsed. Typically, this ToolTip text is the contents of the hidden region.
The language service parser uses the AuthoringSink class to create a list of hidden regions during a full parse operation. The AddHiddenRegions method on the AuthoringSink class adds a new hidden region to the internal list. Later, this list is used in the AddHiddenRegion method of the Source class where the regions are added to the hidden region session supplied by Visual Studio. Once hidden regions are given to the hidden region session, Visual Studio manages the hidden regions for the language service.
The managed package framework (MPF) contains general support for hidden regions and their use in outlining. If you need to determine when the hidden region session is destroyed, a hidden region is changed, or you need to make sure a particular hidden region is visible; you must derive a class from the Source class and override the appropriate methods, OnBeforeSessionEnd, OnHiddenRegionChange, and MakeBaseSpanVisible, respectively.
Example
Here is a simplified example of creating hidden regions for all pairs of braces for the first three levels of nesting. These levels correspond in C# to namespace, class, and method. This approach is not a recommended approach to parsing source code; it is for illustrative purposes only. This example also shows how to temporarily set the AutoOutlining preference to true. An alternative is to specify the AutoOutlining named parameter in the ProvideLanguageServiceAttribute attribute in your language package.
This example assumes C# rules for comments, strings, and literals.
Nota
The MyAuthoringScope class is not shown. For this example, all methods on the MyAuthoringScope class return null values. Also, the GetLineIndexOfPosition method is not shown: this method depends on the source being parsed to identify lines. This method is the equivalent to the same method on the IVsTextLines interface.
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;
namespace MyLanguagePackage
{
internal class TextSpanPriority
{
public TextSpan span;
public int priority;
public TextSpanPriority(TextSpan s, int pri)
{
span = s;
priority = pri;
}
}
public class MyLanguageService : LanguageService
{
//-----------------------------------------------------------
// Private fields.
private LanguagePreferences m_preferences;
private const int MaxNestLevels = 3;
//-----------------------------------------------------------
public override LanguagePreferences GetLanguagePreferences()
{
if (m_preferences == null)
{
m_preferences = new LanguagePreferences(this.Site,
typeof(MyLanguageService).GUID,
Name);
m_preferences.Init();
// Temporarily enabled auto-outlining
m_preferences.AutoOutlining = true;
}
return m_preferences;
}
public override AuthoringScope ParseSource(ParseRequest req)
{
if (req.Sink.HiddenRegions)
{
int index = 0;
int level = 0;
ArrayList regions = new ArrayList();
while (index < req.Text.Length)
{
char c = req.Text[index];
++index;
if (c == '/' && index < req.Text.Length &&
(req.Text[index] == '/' || req.Text[index] == '*'))
{
if (req.Text[index] == '/')
{
// Skip line comments
while (index < req.Text.Length && req.Text[index] != '\n')
{
++index;
}
}
else
{
// Skip block comment.
index = req.Text.IndexOf("*/", index + 1);
if (index != -1)
{
index += "*/".Length;
}
else
{
index = req.Text.Length;
}
}
continue;
}
if (c == '"' || c == '\'')
{
while (index < req.Text.Length)
{
char tc = req.Text[index];
++index;
if (tc == '\\')
{
++index;
}
else if (tc == c)
{
break;
}
}
continue;
}
if (c == '{')
{
if (level < MaxNestLevels)
{
TextSpan span = new TextSpan();
int t = index - 2; // back up to char before {
while (t >= 0 &&
Char.IsWhiteSpace(req.Text[t]))
{
--t;
}
++t;
GetLineIndexOfPosition(t,
out span.iStartLine,
out span.iStartIndex);
regions.Add(new TextSpanPriority(span, level));
}
++level;
}
else if (c == '}')
{
--level;
int i;
for (i = regions.Count - 1; i >= 0; i--)
{
TextSpanPriority sp = (TextSpanPriority)regions[i];
if (sp.priority == level)
{
GetLineIndexOfPosition(index,
out sp.span.iEndLine,
out sp.span.iEndIndex);
if (sp.span.iStartLine != sp.span.iEndLine)
{
req.Sink.AddHiddenRegion(sp.span);
req.Sink.ProcessHiddenRegions = true;
}
regions.Remove(sp);
}
}
}
}
// Must always return a valid AuthoringScope object.
return new MyAuthoringScope();
}
}
}