Formatierung von Kommentaren im C#

Visual Studio 2008 bringt ja von Haus aus schon sehr viele Annehmlichkeiten für einen Software-Entwickler mit sich. Zum Beispiel liebe ich das Refactoring mit all seinen Möglichkeiten, vom Umbenennen von Identifiern bis zum automatischen Implementieren von get-set-Properties. Auch die eher unauffällige Funktion des Macro-Recording (Ctrl-Shift-R) hilft mir häufig dabei, stupide Umstrukturierungsarbeiten etwas angenehmer zu gestalten. Bisher konnte das Visual Studio fast alle produktionssteigernden Mittel anbieten, die wir auch in unserem selbstgebauten Vorgängersystem EMBASSY hatten. Und ein paar wenige Funktionen fehlten mir früher, die nun im Studio vorhanden sind. Beispielsweise die Auswahl von vertikalen Zeichenblöcken mithilfe von Alt-Markieren. Das kann manchmal dabei helfen, wenn man eine Reihe Identifier von Klein- auf Grossschrift ändern will.

Nur eines fing mich langsam an zu stören: Die häufigen Anpassungen von Kommentaren innerhalb der <summary>- und <remarks>-Tags führten zu einem ständigen Neu-Umbrechen der Absätze. Zuerst suchte ich auf dem Internet nach einem automatischen Umbrechen, fand aber nichts. Schon erstaunlich, dachte ich mir, bin ich der einzige, der das vermisst? Ich machte mich also dran, in unserem firmeneigenen VS-Package eine Funktion zu erstellen, die das Umbrechen automatisiert. Als Vorlage hatte ich ja bereits die sehr ausgefeilte Methode zur Verfügung, welche wir im EMBASSY-Editor selbst programmiert hatten und auch tagtäglich brauchten. Das Resultat ist im folgenden C#-Stück zu sehen:

	/// C#-Kommentare ausrichten
	/// Copyright (c) Megos AG 2008
	///
	/// Auszug aus dem Visual Studio Package 'Triton-IDE', welches die Megos für die Software-Entwicklung einsetzt.
	/// Die Lizenz ist Freeware. Jedermann darf den Sourcode verwenden, aber er darf nicht zu
	/// kommerziellen Zwecken vertrieben werden.
	///
 	///
 	/// Hole die grundlegenden Model-Viewer-Elemente und die aktuelle Caretposition des aktuellen Textdokuments.
 	/// Falls etwas schiefgeht, ist eines oder alle Resultatobjekte 'null'.
 	///
 	private void GetTextAndView(out IVsTextLines text, out IVsTextView view, out int caretLineNum, out int caretOffset) {
 		IVsUIShell uiShell;
 		IVsWindowFrame ppWindowFrame;
 		string pbstrData;
 		Object ppunk;
 		IVsTextManager textManager;

			text = null; view = null; caretLineNum = -1; caretOffset = -1;
 		uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
 		uiShell.GetCurrentBFNavigationItem(out ppWindowFrame, out pbstrData, out ppunk);
 		if (ppWindowFrame == null) return;
 		textManager = GetService(typeof(SVsTextManager)) as IVsTextManager;
 		if (textManager == null) return;

			// Mehrere Views können auftreten, wenn das Fenster gesplittet wäre
 		textManager.GetActiveView(1, null, out view);
 		view.GetBuffer(out text);
 		// Entgegen der Doku liefert 'GetCaretPos' hier den Offset ohne expandierte Tabs
 		view.GetCaretPos(out caretLineNum, out caretOffset);
 	}

		public const int ParaWidth = 119;  // Zeichen. Achtung: Kolonnenangabe im Studio ist 1-basiert
 	int tabSize = -1;

		///
 	/// Trenne eine Zeile auf in die Teile [Einrückung + Kommentarsymbol + Leerschläge + Inhalt].
 	/// Leerschläge am Endes des Textinhaltes sind abgeschnitten.
 	/// 'comment' gibt an, welches Kommentarsymbol ("///" oder "//") in 'indent' vorkommt falls nicht 'null'.
 	///
 	private void SplitLine(string line, out string indent, out string comment,
 												 out int contentIndent, out string content) {
 		int pos, contentStart;
 		string rest;

			indent = null; comment = null; content = null;
 		pos = 0;
 		while ((pos < line.Length) && ("\t ".IndexOf(line&#91;pos&#93;) != -1)) {
 			pos++;
 		}
 		indent = line.Substring(0, pos);
 		rest = line.Substring(pos);
 		if (rest.StartsWith("///")) {
 			comment = "///"; pos += 3;
 		} else if (rest.StartsWith("//")) {
 			comment = "//"; pos += 2;
 		}
 		contentStart = pos;
 		while ((pos < line.Length) && (line&#91;pos&#93; == ' ')) pos++;
 		content = line.Substring(pos).TrimEnd();
 		contentIndent = pos - contentStart;
 	}

		///
 	/// Stelle fest, ob die Zeile 'indent'/'comment'/'content' eine Trennzeile darstellt, dh. den auszurichtenden
 	/// Absatz begrenzt. 'paraComment' gibt an, welches Kommentarsymbol die Absatzzeilen haben müssen.
 	///
 	private bool IsSeparatorLine(string indent, string comment, string content, string paraComment) {
 		return (content == "") || (content == "/*") || content == "*/" ||
 						content.StartsWith("{") || content.StartsWith("}") ||
 						((comment == "///") && content.StartsWith("</")) ||
 						content.StartsWith("---") || content.StartsWith("===") ||
 						content.StartsWith("***") || content.StartsWith("*/") ||
 						((paraComment != null) && (comment != paraComment));
 	}

		///
 	/// Stelle fest, ob 'content' die Anfangszeile eines auszurichtenden Block begrenzt. Falls ja, gibt
 	/// 'startTag' an, welches Anfangstag vorliegt (XML oder auch "/*") und 'contentIndent' gibt die
 	/// Einrückung der Inhaltszeilen mit Leerschlägen relativ zum Starttag an.
 	///
 	private bool IsParaStart(string content, out string startTag, out int contentIndent) {
 		int i;

			startTag = ""; contentIndent = 0;
 		if (content.StartsWith("/*")) {
 			startTag = "/*";
 			contentIndent += 2;
 		} else if (content.StartsWith("- ")) {
 			contentIndent += 2;
 		} else if ((content == "") || content == "") {
 			startTag = content;
 			return true;
 		} else if ((content.Length > 0) && (Char.IsDigit(content[0]))) {
 			if ((content.Length > 1) && (Char.IsDigit(content[1]))) {
 				i = 2;
 			} else {
 				i = 1;
 			}
 			if ((content.Length > (i + 1)) && ((content[i] == ')') || (content[i] == '.')) && (content[i + 1] == ' ')) {
 				contentIndent += (i + 2);
 			} else {
 				return false;
 			}
 		} else if ((content.Length > 2) && Char.IsLetter(content[0]) && (content[1] == ')') && (content[2] == ' ')) {
 			contentIndent += 3;
 		} else {
 			return false;
 		}
 		while ((contentIndent < content.Length) && (content&#91;contentIndent&#93; == ' ')) contentIndent++;
 		return true;
 	}

		///
 	/// Hole die Zeile mit einer bestimmten Zeilennummer aus dem Text in Form eines normalen Strings
 	///
 	private bool GetLine(IVsTextLines text, int lineNum, out string line) {
 		LINEDATA&#91;&#93; lineData;
 		int result;

			line = null;
 		lineData = new LINEDATA&#91;1&#93;;
 		result = text.GetLineData(lineNum, lineData, null);
 		if (result != VSConstants.S_OK) return false;
 		line = Marshal.PtrToStringAuto(lineData&#91;0&#93;.pszText, lineData&#91;0&#93;.iLength);
 		return true;
 	}

		///
 	/// Ermittle die Zeilenlänge, so wie sie im Viewer dargestellt wird. Tabulatoren setzen auf die Kolonne
 	/// mit Nummer, die ein Vielfaches von 2 ist.
 	///
 	private int ViewerLineLength(string line) {
 		int len;

			Debug.Assert(tabSize > 0, "TabSize muss eingestellt sein!");
 		len = 0;
 		foreach (char ch in line) {
 			if (ch == '\t') {
 				len = ((len / tabSize) + 1) * tabSize;
 			} else {
 				len++;
 			}
 		}
 		return len;
 	}

		///
    /// This function is the callback used to execute a command when the a menu item is clicked.
    /// See the Initialize method to see how the menu item is associated to this function using
    /// the OleMenuCommandService service and the MenuCommand class.
    ///
 	///
 	/// Richtet den Textinhalt unter dem Absatz des Carets linksbündig aus und bricht an Wortgrenzen um
 	///
    private void JustifyRemark(object sender, EventArgs e) {
 		DTE2 dte2;
 		IVsTextLines text;
 		IVsTextView view;
 		int caretLineNum, caretOffset, lineNum, paraIndent, contentIndent, nextIndent;
 		int firstLineNum, lastLineNum, lastLineLength;  // von Original
 		int secondIndent;  // Einrückung des Inhalts der Zeilen nach der ersten
 		string line, firstIndent, paraComment, comment, content, indent;
 		StringBuilder buf, addLine, output;
 		string startTag, nextTag, bufStr, testLine;
 		ArrayList breaks;
 		bool caretAtEnd, prevBroken, found0, found1, breakPosFound, partAppended;
 		int pos, startPos, pos0, pos1, partLen, prevLen;
 		int newLastLineNum, newLastLineLength;  // von neuem Absatz
 		IntPtr pText;

			dte2 = (DTE2)GetService(typeof(DTE));
 		tabSize = dte2.ActiveDocument.TabSize;

			GetTextAndView(out text, out view, out caretLineNum, out caretOffset);
 		if ((text == null) || (view == null)) return;

			// Zeile, auf dem der Befehl ausgelöst wird analysieren
 		if (!GetLine(text, caretLineNum, out line)) return;
 		SplitLine(line, out firstIndent, out paraComment, out paraIndent, out content);
 		if (IsSeparatorLine(firstIndent, paraComment, content, paraComment)) return;   // direkt auf Separator ausgelöst mache nichts

			// Anfang des Absatzes und Einrückung des Inhalts bestimmen
 		lineNum = caretLineNum;
 		for (; ; ) {
 			if (IsParaStart(content, out startTag, out secondIndent)) break;
 			if (!GetLine(text, lineNum - 1, out line)) break;
 			SplitLine(line, out indent, out comment, out contentIndent, out content);
 			if (IsSeparatorLine(indent, comment, content, paraComment)) break;
 			firstIndent = indent;
 			lineNum--;
 		}
 		if (startTag.StartsWith("<")) lineNum++;  // HTML-Tag
 		firstLineNum = lineNum;  // erste Zeile mit echtem Inhalt

			// Gesamten Text zu einem langen String zusammenhängen
 		buf = new StringBuilder(); breaks = new ArrayList();
 		lastLineNum = 0; lastLineLength = 0;
 		for (; ; ) {
 			if (!GetLine(text, lineNum, out line)) break;
 			SplitLine(line, out indent, out comment, out contentIndent, out content);

				// Auf einen einzigen Leerschlag reduzieren
 			do {
 				prevLen = content.Length;
 				content = content.Replace('\t', ' ').Replace("  ", " ");
 			} while (content.Length  firstLineNum) {
 				if (IsSeparatorLine(indent, comment, content, paraComment)) break;
 				if (IsParaStart(content, out nextTag, out nextIndent)) break;
 				if (buf.Length >= 1) {
 					if ((buf[buf.Length - 1] == '-') &&
 						 !(content.StartsWith("und ") || content.StartsWith("oder "))) {
 						// Klein-Klein zusammenhängen falls hinten nicht "und/oder"
 						if ((buf.Length >= 2) && Char.IsLower(buf[buf.Length - 2]) &&
 								(content.Length > 0) && Char.IsLower(content[0])) {
 							buf.Remove(buf.Length - 1, 1);
 							breaks.Add(buf.Length);
 						}
 					} else {
 						buf.Append(' ');
 					}
 				}
 			}

				// Eigenheit des Studio rückgängig: Innerhalb eines "/*"-Kommentars wird bei einem ENTER automatisch "* " eingefügt
 			if ((startTag == "/*") && content.StartsWith("* ")) {
 				content = content.Substring(2);
 			}

				buf.Append(content);
 			lastLineNum = lineNum;
 			lastLineLength = line.Length;

				lineNum++;
 			if ((startTag == "/*") && content.EndsWith("*/")) break;
 		}
 		caretAtEnd = (caretLineNum + 1 == lineNum) && (caretOffset >= lastLineLength);
 		bufStr = buf.ToString();

			// Generiere nun den Inhalt mit neuer Breite. Dazu jeweils ein weiteres
 		// unzerbrechliches Stück zur Zeile hinzu bis die Breite überschritten wird
 		output = new StringBuilder();
 		addLine = new StringBuilder(firstIndent + paraComment + new String(' ', paraIndent));
 		lineNum = 0; startPos = 0; prevBroken = false; partAppended = false;
 		newLastLineNum = 0; newLastLineLength = 0;
 		while (startPos < bufStr.Length) {
 			// Nächste Umbruchsposition ist ' ', '-' oder ein rausgenommenes '-'
 			pos0 = bufStr.IndexOf(' ', startPos);
 			found0 = pos0 != -1;
 			if (!found0) pos0 = bufStr.Length;

				pos1 = startPos;
 			for (; ; ) {
 				pos1 = bufStr.IndexOf('-', pos1);
 				found1 = pos1 != -1;
 				if (!found1) {
 					pos1 = bufStr.Length; break;
 				}
 				// Symbole "" nicht trennen
 				if ((pos1 > 0) && (bufStr[pos1-1] == '<')) {
 					// "<-"
 				} else if (((pos1+1) ')) {
 					// "->"
 				} else {
 					break;
 				}
 				pos1++;
 			}

				pos = Math.Min(pos0, pos1);
 			if (found0 || found1) pos++;

				// Schauen, ob es noch ein eliminiertes '-' davor gibt
 			breakPosFound = false;
 			foreach (int breakPos in breaks) {
 				if (breakPos >= pos) break;
 				if (breakPos > startPos) {
 					pos = breakPos; breakPosFound = true;
 					break;
 				}
 			}

				// Unzerbrechliches Stück bis und mit nächstem Trennzeichen. Achtung: Es könnte im Part wiederum
 			// Tabulatoren haben! Daher muss man jedes Mal die Viewerlänge von 0 her neu berechnen.
 			partLen = pos - startPos;
 			if ((partLen > 0) && (bufStr[startPos + partLen-1] == ' ')) {
 				// Blank zählt nicht für Test, ob am Ende der Zeile
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen-1);
 			} else if (breakPosFound) {
 				// Trennstrich für Test dazurechnen
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen) + "-";
 			} else {
 				testLine = addLine.ToString() + bufStr.Substring(startPos, partLen);
 			}
 			if ((ViewerLineLength(testLine) > ParaWidth) && (addLine.Length > 0)) {
 				// Stück geht nicht mehr auf Zeile, Zeilenumbruch generieren
 				if (prevBroken) addLine.Append('-');
 				output.Append(addLine.ToString().TrimEnd() + "\r\n");
 				addLine.Remove(0, addLine.Length);
 				addLine.Append(firstIndent + paraComment + new String(' ', paraIndent + secondIndent));
 				newLastLineNum = lineNum;
 				lineNum++;
 			}
 			addLine.Append(bufStr.Substring(startPos, partLen));
 			startPos = pos; prevBroken = breakPosFound; partAppended = true;
 			newLastLineLength = addLine.Length;
 		}
 		// gecachte Zeile noch raus
 		if (partAppended) {
 			output.Append(addLine.ToString() + "\r\n");
 			newLastLineNum = lineNum;
 		}

			// Ersetze den alten Inhalt durch den neuen
 		pText = Marshal.StringToCoTaskMemAuto(output.ToString());  // unmanaged text
 		try {
 			text.ReplaceLines(firstLineNum, 0, lastLineNum+1, 0, pText, output.Length, null);
 		} finally {
 			// in jedem Fall freigeben!
 			Marshal.FreeCoTaskMem(pText);
 		}

			// Caret am Ende soll wieder ans Ende, auch wenn Ende nach unten rechts geht, damit man
 		// direkt weiterschreiben kann
 		newLastLineNum += firstLineNum;
 		if (caretAtEnd) {
 			caretLineNum = newLastLineNum; caretOffset = newLastLineLength;
 		} else {
 			// Caret auf Absatz zurückholen
 			if (caretLineNum > newLastLineNum) {
 				caretLineNum = newLastLineNum;
 				if (caretOffset > newLastLineLength) {
 					caretOffset = newLastLineLength;
 				}
 			} else if (caretOffset > ParaWidth) {
 				caretOffset = ParaWidth;
 			}
 		}
 		view.SetSelection(caretLineNum, caretOffset, caretLineNum, caretOffset);
 		view.CenterColumns(caretLineNum, 0, 1);
 	}

Zur Funktionsweise: Die Funktion erkennt automatisch, um welche Art von Kommentar es sich handelt. Sie kann für fast jede Art von Kommentar im Sourcecode eingesetzt werden. Auch freistehende Absätze ohne Anfangs- und Endmarkierung werden erkannt. Der Umbruch erfolgt bei einem Leerschlag oder Minuszeichen. Minuszeichen, die durch eine andere Umbruchsposition überflüssig geworden sind, werden automatisch wieder entfernt, wobei auf spezielle Wörter wie „und“ und „oder“ geachtet wird. Prinzipiell sollte man einfach mal vertrauen haben in die Funktion und sie benutzen. Sollte ausnahmsweise mal was nicht korrekt umgewandelt werden, kann man die gesamte Umwandlung mit Ctrl-Z rückgängig machen (die gesamte Operation wird vom Studio als ein einziger Veränderungsschritt angeschaut).

Die Funktion fügt man am besten in ein VS Package ein und weist ihr ein Tastaturkürzel zu. Beispielsweise eignen sich F1 bis F12 oder Ctrl-0 bis Ctrl-9 gut, da sie leicht zu erreichen sind.

Leider stellt die hier verwendete Blog-Software den Code nicht exakt so dar, wie er ursprünglich war. Beispielsweise werden die summary-Tags einfach unterschlagen. Lustigerweise geht es in diesem Blog-Beitrag (meinem ersten), aber gerade um das Ausrichten von solchen Bemerkungen. Das trifft sich ja wieder mal gut…(;-)

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: