Custom Document Properties in a Legacy Language Service
Note
This article applies to Visual Studio 2015. If you're looking for the latest Visual Studio documentation, see Visual Studio documentation. We recommend upgrading to the latest version of Visual Studio. Download it here
Document properties can be displayed in the Visual Studio Properties window. Programming languages generally do not have properties associated with individual source files. However, XML supports document properties that affect the encoding, schema, and stylesheet.
Discussion
If your language needs custom document properties, you must derive a class from the DocumentProperties class and implement the necessary properties on your derived class.
In addition, document properties are typically stored in the source file itself. This requires the language service to parse the property information from the source file to display in the Properties window and to update the source file when a change is made to the document properties in the Properties window.
Customizing the DocumentProperties Class
To support custom document properties, you must derive a class from the DocumentProperties class and add as many properties as you need. You should also supply user attributes to organize them in the Properties window display. If a property has only a get
accessor, it is shown as read-only in the Properties window. If a property has both get
and set
accessors, the property can also be updated in the Properties window.
Example
Here is an example class derived from DocumentProperties, showing two properties, Filename and Description. When a property is updated, a custom method on the LanguageService class is called to write the property to the source file.
using System.ComponentModel;
using Microsoft.VisualStudio.Package;
namespace TestLanguagePackage
{
class TestDocumentProperties : DocumentProperties
{
private string m_filename;
private string m_description;
public TestDocumentProperties(CodeWindowManager mgr)
: base(mgr)
{
}
// Helper function to initialize this property without
// going through the FileName property (which does a lot
// more than we need when the filename is set).
public void SetFileName(string filename)
{
m_filename = filename;
}
// Helper function to initialize this property without
// going through the Description property (which does a lot
// more than we need when the description is set).
public void SetDescription(string description)
{
m_description = description;
}
////////////////////////////////////////////////////////////
// The document properties
[CategoryAttribute("General")]
[DescriptionAttribute("Name of the file")]
[DisplayNameAttribute("Filename")]
public string FileName
{
get { return m_filename; }
set
{
if (value != m_filename)
{
m_filename = value;
SetPropertyValue("Filename", m_filename);
}
}
}
[CategoryAttribute("General")]
[DescriptionAttribute("Description of the file")]
[DisplayNameAttribute("Description")]
public string Description
{
get { return m_description; }
set
{
if (value != m_description)
{
m_description = value;
SetPropertyValue("Description", m_description);
}
}
}
///////////////////////////////////////////////////////////
// Private methods.
private void SetPropertyValue(string propertyName, string propertyValue)
{
Source src = this.GetSource();
if (src != null)
{
TestLanguageService service = src.LanguageService as TestLanguageService;
if (service != null)
{
// Set the property in to the source file.
service.SetPropertyValue(src, propertyName, propertyValue);
}
}
}
}
}
Instantiating the Custom DocumentProperties class
To instantiate your custom document properties class, you must override the CreateDocumentProperties method in your version of the LanguageService class to return a single instance of your DocumentProperties class.
Example
using System.ComponentModel;
using Microsoft.VisualStudio.Package;
namespace TestLanguagePackage
{
class TestLanguageService : LanguageService
{
private TestDocumentProperties m_documentProperties;
public override DocumentProperties CreateDocumentProperties(CodeWindowManager mgr)
{
if (m_documentProperties == null)
{
m_documentProperties = new TestDocumentProperties(mgr);
}
return m_documentProperties;
}
}
}
Properties in the Source File
Since document properties are usually specific to the source file, the values are stored in the source file itself. This requires support from the language parser or scanner to define these properties. For example, the properties of an XML document are stored on the root node. The values on the root node are modified when the Properties window values are changed, and the root node is updated in the editor.
Example
This example stores the properties "Filename" and "Description" in the first two lines of the source file, embedded in a special comment header, as:
//!Filename = file.testext
//!Description = A sample file
This example shows the two methods needed to get and set the document properties from the first two lines of the source file as well as how the properties are updated if the user modifies the source file directly. The SetPropertyValue
method in the example shown here is the same one called from the TestDocumentProperties
class as shown in the "Customizing the DocumentProperties class" section.
This example uses the scanner to determine the type of tokens in the first two lines. This example is for illustrative purposes only. A more typical approach to this situation is to parse the source file into what is called a parse tree where each node of the tree contains information about a particular token. The root node would contain the document properties.
using System.ComponentModel;
using Microsoft.VisualStudio.Package;
namespace TestLanguagePackage
{
// TokenType.Comment is the last item in that enumeration.
public enum TestTokenTypes
{
DocPropertyLine = TokenType.Comment + 1,
DocPropertyName,
DocPropertyAssign,
DocPropertyValue
}
class TestLanguageService : LanguageService
{
// Search this many lines from the beginning for properties.
private static int maxLinesToSearch = 2;
private TestDocumentProperties m_documentProperties;
// Called whenever a full parsing operation has completed.
public override void OnParseComplete(ParseRequest req)
{
if (m_documentProperties != null)
{
Source src = GetSource(req.View);
if (src != null)
{
string value = GetPropertyValue(src, "Filename");
m_documentProperties.SetFileName(value);
value = GetPropertyValue(src, "Description");
m_documentProperties.SetDescription(value);
// Update the Properties Window.
m_documentProperties.Refresh();
}
}
}
// Retrieves the specified property value from the given source.
public string GetPropertyValue(Source src, string propertyName)
{
string propertyValue = "";
if (src != null)
{
IVsTextColorState colorState = src.ColorState;
if (colorState != null)
{
string line;
TokenInfo[] lineInfo = null;
int i;
for (i = 0; i < maxLinesToSearch; i++)
{
line = src.GetLine(i);
lineInfo = src.GetColorizer().GetLineInfo(
src.GetTextLines(),
i,
colorState);
if (lineInfo == null)
{
continue;
}
if (lineInfo[0].Type != (TokenType)TestTokenTypes.DocPropertyLine)
{
// Not a property line.
continue;
}
TokenInfo valueInfo = new TokenInfo();
int tokenIndex = -1;
for (tokenIndex = 0;
tokenIndex < lineInfo.Length;
tokenIndex++)
{
if (lineInfo[tokenIndex].Type == (TokenType)TestTokenTypes.DocPropertyName)
{
break;
}
}
if (tokenIndex >= lineInfo.Length)
{
// No property name on the line.
continue;
}
string name = src.GetText(i,
lineInfo[tokenIndex].StartIndex,
i,
lineInfo[tokenIndex].EndIndex + 1);
if (name != null)
{
if (String.Compare(name, propertyName, true) == 0)
{
for ( ;
tokenIndex < lineInfo.Length;
tokenIndex++)
{
if (lineInfo[tokenIndex].Type == (TokenType)TestTokenTypes.DocPropertyValue)
{
break;
}
}
if (tokenIndex < lineInfo.Length)
{
propertyValue = src.GetText(i,
lineInfo[tokenIndex].StartIndex,
i,
lineInfo[tokenIndex].EndIndex + 1);
}
break;
}
}
}
}
}
return propertyValue;
}
// Sets the specified property into the given source file.
// Called from the TestDocumentProperties class.
public void SetPropertyValue(Source src, string propertyName, string propertyValue)
{
string newLine;
if (propertyName == null || propertyName == "")
{
// No property name, so nothing to do
return;
}
if (propertyValue == null)
{
propertyValue = "";
}
// This is the line to be inserted.
newLine = String.Format("//!{0} = {1}", propertyName, propertyValue);
// First, find the line on which the property belongs.
// If line is found, replace it.
// Otherwise, insert the line at the beginning of the file.
if (src != null)
{
IVsTextColorState colorState = src.ColorState;
if (colorState != null)
{
int lineNumber = -1;
string line;
TokenInfo[] lineInfo = null;
int i;
for (i = 0; i < maxLinesToSearch; i++)
{
line = src.GetLine(i);
lineInfo = src.GetColorizer().GetLineInfo(
src.GetTextLines(),
i,
colorState);
if (lineInfo == null)
{
continue;
}
if (lineInfo[0].Type != (TokenType)TestTokenTypes.DocPropertyLine)
{
// Not a property line
continue;
}
TokenInfo valueInfo = new TokenInfo();
int tokenIndex = -1;
for (tokenIndex = 0;
tokenIndex < lineInfo.Length;
tokenIndex++)
{
if (lineInfo[tokenIndex].Type == (TokenType)TestTokenTypes.DocPropertyName)
{
break;
}
}
if (tokenIndex >= lineInfo.Length)
{
// No property name on the line.
continue;
}
string name = src.GetText(i,
lineInfo[tokenIndex].StartIndex,
i,
lineInfo[tokenIndex].EndIndex + 1);
if (name != null)
{
if (String.Compare(name, propertyName, true) == 0)
{
lineNumber = i;
break;
}
}
}
// Set up an undo context that also handles the insert/replace.
EditArray editArray = new EditArray(src,
true,
"Update Property");
if (editArray != null)
{
TextSpan span = new TextSpan();
if (lineNumber != -1)
{
// Replace line.
int lineLength = 0;
src.GetTextLines().GetLengthOfLine(lineNumber,
out lineLength);
span.iStartLine = lineNumber;
span.iStartIndex = 0;
span.iEndLine = lineNumber;
span.iEndIndex = lineLength;
}
else
{
// Insert new line.
span.iStartLine = 0;
span.iStartIndex = 0;
span.iEndLine = 0;
span.iEndIndex = 0;
newLine += "\n";
}
editArray.Add(new EditSpan(span, newLine));
editArray.ApplyEdits();
}
}
}
}
}
}