Tutorial: Mostrar llaves coincidentes
Implemente características basadas en lenguaje, como la coincidencia de llaves mediante la definición de las llaves que desea que coincidan y la adición de una etiqueta de marcador de texto a las llaves coincidentes cuando el símbolo de intercalación está en una de las llaves. Puede definir llaves en el contexto de un idioma, definir su propia extensión de nombre de archivo y tipo de contenido, y aplicar las etiquetas a solo ese tipo o aplicar las etiquetas a un tipo de contenido existente (como "texto"). En el siguiente tutorial se muestra cómo aplicar etiquetas coincidentes de llaves al tipo de contenido "text".
Creación de un proyecto de Managed Extensibility Framework (MEF)
Para crear un nuevo proyecto de MEF
Cree un proyecto de clasificador de editor. Asigne a la solución el nombre
BraceMatchingTest
.Agregue una plantilla de elemento clasificador del editor al proyecto. Para obtener más información, vea Creación de una extensión con una plantilla de elemento de editor.
Elimine los archivos de clase existentes.
Implementación de un tagger coincidente de llaves
Para obtener un efecto de resaltado de llaves similar al que se usa en Visual Studio, puede implementar un tagger de tipo TextMarkerTag. En el código siguiente se muestra cómo definir el tagger para los pares de llaves en cualquier nivel de anidamiento. En este ejemplo, los pares de llaves de [] y {} se definen en el constructor tagger, pero en una implementación de lenguaje completa, los pares de llaves pertinentes se definirían en la especificación del lenguaje.
Para implementar un tagger coincidente de llaves
Agregue un archivo de clase y asígnele el nombre BraceMatching.
Importe los siguientes espacios de nombres.
Defina una clase
BraceMatchingTagger
que herede del ITagger<T> tipo TextMarkerTag.Agregue propiedades para la vista de texto, el búfer de origen, el punto de instantánea actual y también un conjunto de pares de llaves.
En el constructor de etiquetas, establezca las propiedades y suscríbase a los eventos PositionChanged de cambio de vista y LayoutChanged. En este ejemplo, con fines ilustrativos, los pares coincidentes también se definen en el constructor.
internal BraceMatchingTagger(ITextView view, ITextBuffer sourceBuffer) { //here the keys are the open braces, and the values are the close braces m_braceList = new Dictionary<char, char>(); m_braceList.Add('{', '}'); m_braceList.Add('[', ']'); m_braceList.Add('(', ')'); this.View = view; this.SourceBuffer = sourceBuffer; this.CurrentChar = null; this.View.Caret.PositionChanged += CaretPositionChanged; this.View.LayoutChanged += ViewLayoutChanged; }
Como parte de la ITagger<T> implementación, declare un evento TagsChanged.
Los controladores de eventos actualizan la posición de intercalación actual de la
CurrentChar
propiedad y generan el evento TagsChanged.void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { if (e.NewSnapshot != e.OldSnapshot) //make sure that there has really been a change { UpdateAtCaretPosition(View.Caret.Position); } } void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e) { UpdateAtCaretPosition(e.NewPosition); } void UpdateAtCaretPosition(CaretPosition caretPosition) { CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity); if (!CurrentChar.HasValue) return; var tempEvent = TagsChanged; if (tempEvent != null) tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length))); }
Implemente el GetTags método para que coincida con llaves cuando el carácter actual es una llave abierta o cuando el carácter anterior es una llave de cierre, como en Visual Studio. Cuando se encuentra la coincidencia, este método crea una instancia de dos etiquetas, una para la llave abierta y otra para la llave de cierre.
public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans) { if (spans.Count == 0) //there is no content in the buffer yield break; //don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length) yield break; //hold on to a snapshot of the current character SnapshotPoint currentChar = CurrentChar.Value; //if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot if (spans[0].Snapshot != currentChar.Snapshot) { currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive); } //get the current char and the previous char char currentText = currentChar.GetChar(); SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1; //if currentChar is 0 (beginning of buffer), don't move it back char lastText = lastChar.GetChar(); SnapshotSpan pairSpan = new SnapshotSpan(); if (m_braceList.ContainsKey(currentText)) //the key is the open brace { char closeChar; m_braceList.TryGetValue(currentText, out closeChar); if (BraceMatchingTagger.FindMatchingCloseChar(currentChar, currentText, closeChar, View.TextViewLines.Count, out pairSpan) == true) { yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), new TextMarkerTag("blue")); yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue")); } } else if (m_braceList.ContainsValue(lastText)) //the value is the close brace, which is the *previous* character { var open = from n in m_braceList where n.Value.Equals(lastText) select n.Key; if (BraceMatchingTagger.FindMatchingOpenChar(lastChar, (char)open.ElementAt<char>(0), lastText, View.TextViewLines.Count, out pairSpan) == true) { yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), new TextMarkerTag("blue")); yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue")); } } }
Los siguientes métodos privados encuentran la llave coincidente en cualquier nivel de anidamiento. El primer método busca el carácter de cierre que coincide con el carácter abierto:
private static bool FindMatchingCloseChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan) { pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1); ITextSnapshotLine line = startPoint.GetContainingLine(); string lineText = line.GetText(); int lineNumber = line.LineNumber; int offset = startPoint.Position - line.Start.Position + 1; int stopLineNumber = startPoint.Snapshot.LineCount - 1; if (maxLines > 0) stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines); int openCount = 0; while (true) { //walk the entire line while (offset < line.Length) { char currentChar = lineText[offset]; if (currentChar == close) //found the close character { if (openCount > 0) { openCount--; } else //found the matching close { pairSpan = new SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1); return true; } } else if (currentChar == open) // this is another open { openCount++; } offset++; } //move on to the next line if (++lineNumber > stopLineNumber) break; line = line.Snapshot.GetLineFromLineNumber(lineNumber); lineText = line.GetText(); offset = 0; } return false; }
El siguiente método auxiliar busca el carácter abierto que coincide con un carácter cercano:
private static bool FindMatchingOpenChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan) { pairSpan = new SnapshotSpan(startPoint, startPoint); ITextSnapshotLine line = startPoint.GetContainingLine(); int lineNumber = line.LineNumber; int offset = startPoint - line.Start - 1; //move the offset to the character before this one //if the offset is negative, move to the previous line if (offset < 0) { line = line.Snapshot.GetLineFromLineNumber(--lineNumber); offset = line.Length - 1; } string lineText = line.GetText(); int stopLineNumber = 0; if (maxLines > 0) stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines); int closeCount = 0; while (true) { // Walk the entire line while (offset >= 0) { char currentChar = lineText[offset]; if (currentChar == open) { if (closeCount > 0) { closeCount--; } else // We've found the open character { pairSpan = new SnapshotSpan(line.Start + offset, 1); //we just want the character itself return true; } } else if (currentChar == close) { closeCount++; } offset--; } // Move to the previous line if (--lineNumber < stopLineNumber) break; line = line.Snapshot.GetLineFromLineNumber(lineNumber); lineText = line.GetText(); offset = line.Length - 1; } return false; }
Implementación de un proveedor de etiquetas de coincidencia de llaves
Además de implementar un tagger, también debe implementar y exportar un proveedor de etiquetas. En este caso, el tipo de contenido del proveedor es "text". Por lo tanto, la coincidencia de llaves aparecerá en todos los tipos de archivos de texto, pero una implementación más completa aplica la coincidencia de llaves solo a un tipo de contenido específico.
Para implementar un proveedor de etiquetas coincidente de llaves
Declare un proveedor de etiquetas que herede de IViewTaggerProvider, asígnele el nombre BraceMatchingTaggerProvider y expórtelo con un ContentTypeAttribute de "texto" y un TagTypeAttribute de TextMarkerTag.
Implemente el CreateTagger método para crear una instancia de BraceMatchingTagger.
Compilación y prueba del código
Para probar este código, compile la solución BraceMatchingTest y ejecútelo en la instancia experimental.
Para compilar y probar la solución BraceMatchingTest
Compile la solución.
Al ejecutar este proyecto en el depurador, se inicia una segunda instancia de Visual Studio.
Cree un archivo de texto y escriba texto que incluya llaves coincidentes.
hello { goodbye} {} {hello}
Al colocar el símbolo de intercalación antes de una llave abierta, debe resaltarse tanto esa llave como la llave de cierre coincidente. Al colocar el cursor justo después de la llave de cierre, debe resaltarse tanto esa llave como la llave abierta coincidente.