Procedura dettagliata: Definizione della struttura
Articolo
Configurare le funzionalità basate sul linguaggio, ad esempio la struttura, definendo i tipi di aree di testo da espandere o comprimere. È possibile definire aree nel contesto di un servizio linguistico oppure definire il proprio tipo di contenuto e estensione del nome file e applicare la definizione dell'area solo a tale tipo oppure applicare le definizioni di area a un tipo di contenuto esistente,ad esempio "text". Questa procedura dettagliata illustra come definire e visualizzare le aree di struttura.
Creare un progetto MEF (Managed Extensibility Framework)
Per creare un progetto MEF
Creare un progetto VSIX. Assegnare alla soluzione il nome OutlineRegionTest.
Aggiungere un modello di elemento classificatore dell'editor al progetto. Per altre informazioni, vedere Creare un'estensione con un modello di elemento dell'editor.
Eliminare i file di classe esistenti.
Implementare un tagger di struttura
Le aree di struttura sono contrassegnate da un tipo di tag (OutliningRegionTag). Questo tag fornisce il comportamento di struttura standard. L'area evidenziata può essere espansa o compressa. L'area evidenziata è contrassegnata da un segno Più (+) se è compresso o un segno meno (-) se viene espanso e l'area espansa viene demarcata da una linea verticale.
I passaggi seguenti illustrano come definire un tagger che crea aree di struttura per tutte le aree delimitate dalle parentesi quadre ([,]).
Per implementare un tagger di struttura
Aggiungere un file di classe e assegnargli il nome OutliningTagger.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Text;
internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
Friend NotInheritable Class OutliningTagger
Implements ITagger(Of IOutliningRegionTag)
Aggiungere alcuni campi per tenere traccia del buffer di testo e dello snapshot e per accumulare i set di righe che devono essere contrassegnati come aree strutturate. Questo codice include un elenco di oggetti Region (da definire in un secondo momento) che rappresentano le aree di struttura.
string startHide = "["; //the characters that start the outlining region
string endHide = "]"; //the characters that end the outlining region
string ellipsis = "..."; //the characters that are displayed when the region is collapsed
string hoverText = "hover text"; //the contents of the tooltip for the collapsed span
ITextBuffer buffer;
ITextSnapshot snapshot;
List<Region> regions;
'the characters that start the outlining region
Private startHide As String = "["
'the characters that end the outlining region
Private endHide As String = "]"
'the characters that are displayed when the region is collapsed
Private ellipsis As String = "..."
'the contents of the tooltip for the collapsed span
Private hoverText As String = "hover text"
Private buffer As ITextBuffer
Private snapshot As ITextSnapshot
Private regions As List(Of Region)
Aggiungere un costruttore tagger che inizializza i campi, analizza il buffer e aggiunge un gestore eventi all'evento Changed .
public OutliningTagger(ITextBuffer buffer)
{
this.buffer = buffer;
this.snapshot = buffer.CurrentSnapshot;
this.regions = new List<Region>();
this.ReParse();
this.buffer.Changed += BufferChanged;
}
Public Sub New(ByVal buffer As ITextBuffer)
Me.buffer = buffer
Me.snapshot = buffer.CurrentSnapshot
Me.regions = New List(Of Region)()
Me.ReParse()
AddHandler Me.buffer.Changed, AddressOf BufferChanged
End Sub
Implementare il GetTags metodo , che crea un'istanza dell'intervallo di tag. In questo esempio si presuppone che gli intervalli nell'oggetto NormalizedSpanCollection passato al metodo siano contigui, anche se potrebbe non essere sempre il caso. Questo metodo crea un'istanza di un nuovo intervallo di tag per ogni area di struttura.
public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (spans.Count == 0)
yield break;
List<Region> currentRegions = this.regions;
ITextSnapshot currentSnapshot = this.snapshot;
SnapshotSpan entire = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive);
int startLineNumber = entire.Start.GetContainingLine().LineNumber;
int endLineNumber = entire.End.GetContainingLine().LineNumber;
foreach (var region in currentRegions)
{
if (region.StartLine <= endLineNumber &&
region.EndLine >= startLineNumber)
{
var startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine);
var endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine);
//the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
yield return new TagSpan<IOutliningRegionTag>(
new SnapshotSpan(startLine.Start + region.StartOffset,
endLine.End),
new OutliningRegionTag(false, false, ellipsis, hoverText));
}
}
}
Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of IOutliningRegionTag)) Implements ITagger(Of Microsoft.VisualStudio.Text.Tagging.IOutliningRegionTag).GetTags
If spans.Count = 0 Then
Return Nothing
Exit Function
End If
Dim currentRegions As List(Of Region) = Me.regions
Dim currentSnapshot As ITextSnapshot = Me.snapshot
Dim entire As SnapshotSpan = New SnapshotSpan(spans(0).Start, spans(spans.Count - 1).[End]).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive)
Dim startLineNumber As Integer = entire.Start.GetContainingLine().LineNumber
Dim endLineNumber As Integer = entire.[End].GetContainingLine().LineNumber
Dim list As List(Of ITagSpan(Of IOutliningRegionTag))
list = New List(Of ITagSpan(Of IOutliningRegionTag))()
For Each region In currentRegions
If region.StartLine <= endLineNumber AndAlso region.EndLine >= startLineNumber Then
Dim startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine)
Dim endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine)
'the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
list.Add(New TagSpan(Of IOutliningRegionTag)(New SnapshotSpan(startLine.Start + region.StartOffset, endLine.End),
New OutliningRegionTag(False, False, ellipsis, hoverText)))
End If
Next
Return list
End Function
void BufferChanged(object sender, TextContentChangedEventArgs e)
{
// If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
if (e.After != buffer.CurrentSnapshot)
return;
this.ReParse();
}
Private Sub BufferChanged(ByVal sender As Object, ByVal e As TextContentChangedEventArgs)
' If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
If e.After IsNot buffer.CurrentSnapshot Then
Exit Sub
End If
Me.ReParse()
End Sub
Aggiungere un metodo che analizza il buffer. L'esempio riportato qui è solo per l'illustrazione. Analizza in modo sincrono il buffer in aree di struttura nidificate.
void ReParse()
{
ITextSnapshot newSnapshot = buffer.CurrentSnapshot;
List<Region> newRegions = new List<Region>();
//keep the current (deepest) partial region, which will have
// references to any parent partial regions.
PartialRegion currentRegion = null;
foreach (var line in newSnapshot.Lines)
{
int regionStart = -1;
string text = line.GetText();
//lines that contain a "[" denote the start of a new region.
if ((regionStart = text.IndexOf(startHide, StringComparison.Ordinal)) != -1)
{
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
int newLevel;
if (!TryGetLevel(text, regionStart, out newLevel))
newLevel = currentLevel + 1;
//levels are the same and we have an existing region;
//end the current region and start the next
if (currentLevel == newLevel && currentRegion != null)
{
newRegions.Add(new Region()
{
Level = currentRegion.Level,
StartLine = currentRegion.StartLine,
StartOffset = currentRegion.StartOffset,
EndLine = line.LineNumber
});
currentRegion = new PartialRegion()
{
Level = newLevel,
StartLine = line.LineNumber,
StartOffset = regionStart,
PartialParent = currentRegion.PartialParent
};
}
//this is a new (sub)region
else
{
currentRegion = new PartialRegion()
{
Level = newLevel,
StartLine = line.LineNumber,
StartOffset = regionStart,
PartialParent = currentRegion
};
}
}
//lines that contain "]" denote the end of a region
else if ((regionStart = text.IndexOf(endHide, StringComparison.Ordinal)) != -1)
{
int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
int closingLevel;
if (!TryGetLevel(text, regionStart, out closingLevel))
closingLevel = currentLevel;
//the regions match
if (currentRegion != null &&
currentLevel == closingLevel)
{
newRegions.Add(new Region()
{
Level = currentLevel,
StartLine = currentRegion.StartLine,
StartOffset = currentRegion.StartOffset,
EndLine = line.LineNumber
});
currentRegion = currentRegion.PartialParent;
}
}
}
//determine the changed span, and send a changed event with the new spans
List<Span> oldSpans =
new List<Span>(this.regions.Select(r => AsSnapshotSpan(r, this.snapshot)
.TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive)
.Span));
List<Span> newSpans =
new List<Span>(newRegions.Select(r => AsSnapshotSpan(r, newSnapshot).Span));
NormalizedSpanCollection oldSpanCollection = new NormalizedSpanCollection(oldSpans);
NormalizedSpanCollection newSpanCollection = new NormalizedSpanCollection(newSpans);
//the changed regions are regions that appear in one set or the other, but not both.
NormalizedSpanCollection removed =
NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection);
int changeStart = int.MaxValue;
int changeEnd = -1;
if (removed.Count > 0)
{
changeStart = removed[0].Start;
changeEnd = removed[removed.Count - 1].End;
}
if (newSpans.Count > 0)
{
changeStart = Math.Min(changeStart, newSpans[0].Start);
changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End);
}
this.snapshot = newSnapshot;
this.regions = newRegions;
if (changeStart <= changeEnd)
{
ITextSnapshot snap = this.snapshot;
if (this.TagsChanged != null)
this.TagsChanged(this, new SnapshotSpanEventArgs(
new SnapshotSpan(this.snapshot, Span.FromBounds(changeStart, changeEnd))));
}
}
Private Sub ReParse()
Dim newSnapshot As ITextSnapshot = buffer.CurrentSnapshot
Dim newRegions As New List(Of Region)()
'keep the current (deepest) partial region, which will have
' references to any parent partial regions.
Dim currentRegion As PartialRegion = Nothing
For Each line In newSnapshot.Lines
Dim regionStart As Integer = -1
Dim text As String = line.GetText()
'lines that contain a "[" denote the start of a new region.
If text.IndexOf(startHide, StringComparison.Ordinal) <> -1 Then
regionStart = text.IndexOf(startHide, StringComparison.Ordinal)
Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
Dim newLevel As Integer
If Not TryGetLevel(text, regionStart, newLevel) Then
newLevel = currentLevel + 1
End If
'levels are the same and we have an existing region;
'end the current region and start the next
If currentLevel = newLevel AndAlso currentRegion IsNot Nothing Then
Dim newRegion = New Region()
newRegion.Level = currentRegion.Level
newRegion.StartLine = currentRegion.StartLine
newRegion.StartOffset = currentRegion.StartOffset
newRegion.EndLine = line.LineNumber
newRegions.Add(newRegion)
currentRegion = New PartialRegion()
currentRegion.Level = newLevel
currentRegion.StartLine = line.LineNumber
currentRegion.StartOffset = regionStart
currentRegion.PartialParent = currentRegion.PartialParent
Else
'this is a new (sub)region
currentRegion = New PartialRegion()
currentRegion.Level = newLevel
currentRegion.StartLine = line.LineNumber
currentRegion.StartOffset = regionStart
currentRegion.PartialParent = currentRegion
End If
'lines that contain "]" denote the end of a region
ElseIf (text.IndexOf(endHide, StringComparison.Ordinal)) <> -1 Then
regionStart = text.IndexOf(endHide, StringComparison.Ordinal)
Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
Dim closingLevel As Integer
If Not TryGetLevel(text, regionStart, closingLevel) Then
closingLevel = currentLevel
End If
'the regions match
If currentRegion IsNot Nothing AndAlso currentLevel = closingLevel Then
Dim newRegion As Region
newRegion = New Region()
newRegion.Level = currentLevel
newRegion.StartLine = currentRegion.StartLine
newRegion.StartOffset = currentRegion.StartOffset
newRegion.EndLine = line.LineNumber
newRegions.Add(newRegion)
currentRegion = currentRegion.PartialParent
End If
End If
Next
'determine the changed span, and send a changed event with the new spans
Dim oldSpans As New List(Of Span)(Me.regions.[Select](Function(r) AsSnapshotSpan(r, Me.snapshot).TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive).Span))
Dim newSpans As New List(Of Span)(newRegions.[Select](Function(r) AsSnapshotSpan(r, newSnapshot).Span))
Dim oldSpanCollection As New NormalizedSpanCollection(oldSpans)
Dim newSpanCollection As New NormalizedSpanCollection(newSpans)
'the changed regions are regions that appear in one set or the other, but not both.
Dim removed As NormalizedSpanCollection = NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection)
Dim changeStart As Integer = Integer.MaxValue
Dim changeEnd As Integer = -1
If removed.Count > 0 Then
changeStart = removed(0).Start
changeEnd = removed(removed.Count - 1).[End]
End If
If newSpans.Count > 0 Then
changeStart = Math.Min(changeStart, newSpans(0).Start)
changeEnd = Math.Max(changeEnd, newSpans(newSpans.Count - 1).[End])
End If
Me.snapshot = newSnapshot
Me.regions = newRegions
If changeStart <= changeEnd Then
Dim snap As ITextSnapshot = Me.snapshot
RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(Me.snapshot, Span.FromBounds(changeStart, changeEnd))))
End If
End Sub
Il metodo helper seguente ottiene un numero intero che rappresenta il livello della struttura, in modo che 1 sia la coppia parentesi graffa più a sinistra.
static bool TryGetLevel(string text, int startIndex, out int level)
{
level = -1;
if (text.Length > startIndex + 3)
{
if (int.TryParse(text.Substring(startIndex + 1), out level))
return true;
}
return false;
}
Private Shared Function TryGetLevel(ByVal text As String, ByVal startIndex As Integer, ByRef level As Integer) As Boolean
level = -1
If text.Length > startIndex + 3 Then
If Integer.TryParse(text.Substring(startIndex + 1), level) Then
Return True
End If
End If
Return False
End Function
Il metodo helper seguente converte un'area (definita più avanti in questo articolo) in snapshotSpan.
static SnapshotSpan AsSnapshotSpan(Region region, ITextSnapshot snapshot)
{
var startLine = snapshot.GetLineFromLineNumber(region.StartLine);
var endLine = (region.StartLine == region.EndLine) ? startLine
: snapshot.GetLineFromLineNumber(region.EndLine);
return new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End);
}
Private Shared Function AsSnapshotSpan(ByVal region As Region, ByVal snapshot As ITextSnapshot) As SnapshotSpan
Dim startLine = snapshot.GetLineFromLineNumber(region.StartLine)
Dim endLine = If((region.StartLine = region.EndLine), startLine, snapshot.GetLineFromLineNumber(region.EndLine))
Return New SnapshotSpan(startLine.Start + region.StartOffset, endLine.[End])
End Function
Il codice seguente è solo per l'illustrazione. Definisce una classe PartialRegion che contiene il numero di riga e l'offset dell'inizio di un'area struttura e un riferimento all'area padre (se presente). Questo codice consente al parser di configurare le aree di struttura annidate. Una classe Region derivata contiene un riferimento al numero di riga della fine di un'area struttura.
class PartialRegion
{
public int StartLine { get; set; }
public int StartOffset { get; set; }
public int Level { get; set; }
public PartialRegion PartialParent { get; set; }
}
class Region : PartialRegion
{
public int EndLine { get; set; }
}
Private Class PartialRegion
Private _StartLine As Integer
Public Property StartLine() As Integer
Get
Return _StartLine
End Get
Set(ByVal value As Integer)
_StartLine = value
End Set
End Property
Private _StartOffset As Integer
Public Property StartOffset() As Integer
Get
Return _StartOffset
End Get
Set(ByVal value As Integer)
_StartOffset = value
End Set
End Property
Private _Level As Integer
Public Property Level() As Integer
Get
Return _Level
End Get
Set(ByVal value As Integer)
_Level = value
End Set
End Property
Private _PartialParent As PartialRegion
Public Property PartialParent() As PartialRegion
Get
Return _PartialParent
End Get
Set(ByVal value As PartialRegion)
_PartialParent = value
End Set
End Property
End Class
Private Class Region
Inherits PartialRegion
Private _EndLine As Integer
Public Property EndLine() As Integer
Get
Return _EndLine
End Get
Set(ByVal value As Integer)
_EndLine = value
End Set
End Property
End Class
Implementare un provider di tagger
Esportare un provider di tagger per il tagger. Il provider di tagger crea un oggetto OutliningTagger per un buffer del tipo di contenuto "text" oppure restituisce un valore OutliningTagger se il buffer ne ha già uno.
Per implementare un provider di tagger
Creare una classe denominata OutliningTaggerProvider che implementa ITaggerProvidered esportarla con gli attributi ContentType e TagType.
public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
{
//create a single tagger for each buffer.
Func<ITagger<T>> sc = delegate() { return new OutliningTagger(buffer) as ITagger<T>; };
return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
}
Public Function CreateTagger(Of T As ITag)(ByVal buffer As ITextBuffer) As ITagger(Of T) Implements ITaggerProvider.CreateTagger
'create a single tagger for each buffer.
Dim sc As Func(Of ITagger(Of T)) = Function() TryCast(New OutliningTagger(buffer), ITagger(Of T))
Return buffer.Properties.GetOrCreateSingletonProperty(Of ITagger(Of T))(sc)
End Function
Compilare e testare il codice
Per testare questo codice, compilare la soluzione OutlineRegionTest ed eseguirla nell'istanza sperimentale.
Per compilare e testare la soluzione OutlineRegionTest
Compilare la soluzione.
Quando si esegue questo progetto nel debugger, viene avviata una seconda istanza di Visual Studio.
Creare un file di testo. Digitare un testo che includa sia le parentesi quadre di apertura che le parentesi quadre di chiusura.
[
Hello
]
Deve essere presente un'area di struttura che include entrambe le parentesi quadre. Dovrebbe essere possibile fare clic sul segno meno a sinistra della parentesi aperta per comprimere l'area di struttura. Quando l'area è compressa, il simbolo con i puntini di sospensione (...) dovrebbe apparire a sinistra dell'area compressa e quando si sposta il puntatore del puntatore sui puntini di sospensione verrà visualizzato un popup contenente il testo al passaggio del testo.