Click here to Skip to main content
15,891,431 members
Articles / Desktop Programming / Windows Forms

Line Counter - Writing a Visual Studio 2005 & 2008 Add-In

Rate me:
Please Sign up or sign in to vote.
4.88/5 (124 votes)
13 May 2009CPOL25 min read 823.3K   17.8K   448  
Provides a complete Solution and Project line counting and summary tool, written as a Visual Studio 2005/2008 Managed Add-in.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.IO;

using Microsoft.VisualStudio.CommandBars;

using Extensibility;
using EnvDTE;
using EnvDTE80;

using LineCounterAddin.Interfaces;

namespace LineCounterAddin
{
	/// <summary>
	/// Line Counter add-in user interface.
	/// </summary>
	public partial class LineCounterBrowser : UserControl
	{
		#region Nested Classes
		abstract class ListViewItemComparer : System.Collections.IComparer
		{
			public abstract int Compare(ListViewItem item1, ListViewItem item2);

			public ListView SortingList;
			public int Column;

			#region IComparer Members
			int System.Collections.IComparer.Compare(object x, object y)
			{
				if (x is ListViewItem && y is ListViewItem)
				{
					int diff = Compare((ListViewItem)x, (ListViewItem)y);
					if (SortingList.Sorting == SortOrder.Descending)
						diff *= -1;

					return diff;
				}
				else
				{
					throw new ArgumentException("One or both of the arguments are not ListViewItem objects.");
				}
			}
			#endregion
		}

		/// <summary>
		/// Compares items based on file name.
		/// </summary>
		class FileNameComparer : ListViewItemComparer
		{

			public override int Compare(ListViewItem item1, ListViewItem item2)
			{
				return String.Compare(item1.Text, item2.Text, false);
			}
		}

		/// <summary>
		/// Compares items based on total lines primarily, and
		/// the filename secondarily.
		/// </summary>
		class FileLinesComparer : ListViewItemComparer
		{

			public override int Compare(ListViewItem item1, ListViewItem item2)
			{
				string string1 = item1.SubItems[Column].Text;
				string string2 = item2.SubItems[Column].Text;

				if (string1 != null && string2 != null)
				{
					int total1 = int.Parse(string1);
					int total2 = int.Parse(string2);

					// Compare the totals...
					int diff = total1 - total2;

					// If totals are equal...
					if (diff == 0)
					{
						// Compare the filenames
						diff = String.Compare(item1.Text, item2.Text, false);
					}

					return diff;
				}

				return 0;
			}
		}

		/// <summary>
		/// Compares items based on file extension.
		/// </summary>
		class FileExtensionComparer : ListViewItemComparer
		{

			public override int Compare(ListViewItem item1, ListViewItem item2)
			{
				string string1 = item1.SubItems[4].Text;
				string string2 = item2.SubItems[4].Text;

				return String.Compare(string1, string2, true);
			}
		}
		#endregion

		#region Constructor
		/// <summary>
		/// Construct the line counter user interface and
		/// the countable file type mappings (to icons and
		/// counting algorithms).
		/// </summary>
		public LineCounterBrowser()
		{
			InitializeComponent();

			m_cfgMgr = ConfigManager.Instance;

			#region Obsolete Initialization
			// Map project types to icons for use in the projects list
			/*m_projIconMappings = new Dictionary<string, int>();
			m_projIconMappings.Add("{00000000-0000-0000-0000-000000000000}", 0);
			m_projIconMappings.Add(CodeModelLanguageConstants.vsCMLanguageCSharp, 1);
			m_projIconMappings.Add(CodeModelLanguageConstants.vsCMLanguageVB, 2);
			m_projIconMappings.Add(CodeModelLanguageConstants.vsCMLanguageMC, 3);
			m_projIconMappings.Add(CodeModelLanguageConstants.vsCMLanguageVC, 3);
			m_projIconMappings.Add("{00000001-0000-0000-0000-000000000000}", 5);*/

			// List all the countable file types (so we don't try to count .dll's,
			// images, executables, etc.
			/*m_countableTypes = new System.Collections.Specialized.StringCollection();
			m_countableTypes.Add("*");
			m_countableTypes.Add(".cs");
			m_countableTypes.Add(".vb");
			m_countableTypes.Add(".vj");
			m_countableTypes.Add(".cpp");
			m_countableTypes.Add(".cc");
			m_countableTypes.Add(".cxx");
			m_countableTypes.Add(".c");
			m_countableTypes.Add(".hpp");
			m_countableTypes.Add(".hh");
			m_countableTypes.Add(".hxx");
			m_countableTypes.Add(".h");
			m_countableTypes.Add(".js");
			m_countableTypes.Add(".cd");
			m_countableTypes.Add(".resx");
			m_countableTypes.Add(".res");
			m_countableTypes.Add(".css");
			m_countableTypes.Add(".htm");
			m_countableTypes.Add(".html");
			m_countableTypes.Add(".xml");
			m_countableTypes.Add(".xsl");
			m_countableTypes.Add(".xslt");
			m_countableTypes.Add(".xsd");
			m_countableTypes.Add(".config");
			m_countableTypes.Add(".asax");
			m_countableTypes.Add(".ascx");
			m_countableTypes.Add(".asmx");
			m_countableTypes.Add(".aspx");
			m_countableTypes.Add(".ashx");
			m_countableTypes.Add(".idl");
			m_countableTypes.Add(".odl");
			m_countableTypes.Add(".txt");
			m_countableTypes.Add(".sql");*/

			// Map file extensions to icons for use in the file list
			/*m_fileIconMappings = new Dictionary<string, int>(33);
			m_fileIconMappings.Add("*", 0);
			m_fileIconMappings.Add(".cs", 1);
			m_fileIconMappings.Add(".vb", 2);
			m_fileIconMappings.Add(".vj", 3);
			m_fileIconMappings.Add(".cpp", 4);
			m_fileIconMappings.Add(".cc", 4);
			m_fileIconMappings.Add(".cxx", 4);
			m_fileIconMappings.Add(".c", 5);
			m_fileIconMappings.Add(".hpp", 6);
			m_fileIconMappings.Add(".hh", 6);
			m_fileIconMappings.Add(".hxx", 6);
			m_fileIconMappings.Add(".h", 6);
			m_fileIconMappings.Add(".js", 7);
			m_fileIconMappings.Add(".cd", 8);
			m_fileIconMappings.Add(".resx", 9);
			m_fileIconMappings.Add(".res", 9);
			m_fileIconMappings.Add(".css", 10);
			m_fileIconMappings.Add(".htm", 11);
			m_fileIconMappings.Add(".html", 11);
			m_fileIconMappings.Add(".xml", 12);
			m_fileIconMappings.Add(".xsl", 13);
			m_fileIconMappings.Add(".xslt", 13);
			m_fileIconMappings.Add(".xsd", 14);
			m_fileIconMappings.Add(".config", 15);
			m_fileIconMappings.Add(".asax", 16);
			m_fileIconMappings.Add(".ascx", 17);
			m_fileIconMappings.Add(".asmx", 18);
			m_fileIconMappings.Add(".aspx", 19);
			m_fileIconMappings.Add(".ashx", 0);
			m_fileIconMappings.Add(".idl", 0);
			m_fileIconMappings.Add(".odl", 0);
			m_fileIconMappings.Add(".txt", 0);			
			m_fileIconMappings.Add(".sql", 0);*/

			// Prepare counting algorithm mappings
			/*CountParserDelegate countLinesGeneric = new CountParserDelegate(CountLinesGeneric);
			CountParserDelegate countLinesCStyle = new CountParserDelegate(CountLinesCStyle);
			CountParserDelegate countLinesVBStyle = new CountParserDelegate(CountLinesVBStyle);
			CountParserDelegate countLinesXMLStyle = new CountParserDelegate(CountLinesXMLStyle);

			m_countAlgorithms = new Dictionary<string, CountParserDelegate>(33);
			m_countAlgorithms.Add("*", countLinesGeneric);
			m_countAlgorithms.Add(".cs", countLinesCStyle);
			m_countAlgorithms.Add(".vb", countLinesVBStyle);
			m_countAlgorithms.Add(".vj", countLinesCStyle);
			m_countAlgorithms.Add(".js", countLinesCStyle);
			m_countAlgorithms.Add(".cpp", countLinesCStyle);
			m_countAlgorithms.Add(".cc", countLinesCStyle);
			m_countAlgorithms.Add(".cxx", countLinesCStyle);
			m_countAlgorithms.Add(".c", countLinesCStyle);
			m_countAlgorithms.Add(".hpp", countLinesCStyle);
			m_countAlgorithms.Add(".hh", countLinesCStyle);
			m_countAlgorithms.Add(".hxx", countLinesCStyle);
			m_countAlgorithms.Add(".h", countLinesCStyle);
			m_countAlgorithms.Add(".idl", countLinesCStyle);
			m_countAlgorithms.Add(".odl", countLinesCStyle);
			m_countAlgorithms.Add(".txt", countLinesGeneric);
			m_countAlgorithms.Add(".xml", countLinesXMLStyle);
			m_countAlgorithms.Add(".xsl", countLinesXMLStyle);
			m_countAlgorithms.Add(".xslt", countLinesXMLStyle);
			m_countAlgorithms.Add(".xsd", countLinesXMLStyle);
			m_countAlgorithms.Add(".config", countLinesXMLStyle);
			m_countAlgorithms.Add(".res", countLinesGeneric);
			m_countAlgorithms.Add(".resx", countLinesXMLStyle);
			m_countAlgorithms.Add(".aspx", countLinesXMLStyle);
			m_countAlgorithms.Add(".ascx", countLinesXMLStyle);
			m_countAlgorithms.Add(".ashx", countLinesXMLStyle);
			m_countAlgorithms.Add(".asmx", countLinesXMLStyle);
			m_countAlgorithms.Add(".asax", countLinesXMLStyle);
			m_countAlgorithms.Add(".htm", countLinesXMLStyle);
			m_countAlgorithms.Add(".html", countLinesXMLStyle);
			m_countAlgorithms.Add(".css", countLinesCStyle);
			m_countAlgorithms.Add(".sql", countLinesGeneric);
			m_countAlgorithms.Add(".cd", countLinesGeneric);*/
			#endregion
		}
		#endregion

		#region Variables
		private DTE2 m_dte;			// Reference to the Visual Studio DTE object
		private List<LineCountSummary> m_summaryList;
		//private Dictionary<string, int> m_projIconMappings;
		//private Dictionary<string, int> m_fileIconMappings;
		private Dictionary<string, CountParserDelegate> m_countAlgorithms;
		private System.Collections.Specialized.StringCollection m_countableTypes;

		private ConfigManager m_cfgMgr;
		#endregion

		#region Properties
		/// <summary>
		/// Recieves the VS DTE object
		/// </summary>
		public DTE2 DTE
		{
			set 
			{
				m_dte = value;
			}
		}
		#endregion

		#region Handlers
		private int lastSortColumn = -1;	// Track the last clicked column

		/// <summary>
		/// Sorts the ListView by the clicked column, automatically
		/// reversing the sort order on subsequent clicks of the 
		/// same column.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e">Provides the index of the clicked column.</param>
		private void lvFileList_ColumnClick(object sender, ColumnClickEventArgs e)
		{
			// Define a variable of the abstract (generic) comparer
			ListViewItemComparer comparer = null;

			// Create an instance of the specific comparer in the 'comparer' 
			// variable. Since each of the explicit comparer classes is
			// derived from the abstract case class, polymorphism applies.
			switch (e.Column)
			{
				// Line count columns
				case 1:
				case 2:
				case 3:
				comparer = new FileLinesComparer();
				break;
				// The file extension column
				case 4:
				comparer = new FileExtensionComparer();
				break;
				// All other columns sort by file name
				default:
				comparer = new FileNameComparer();
				break;
			}

			// Set the sorting order
			if (lastSortColumn == e.Column)
			{
				if (lvFileList.Sorting == SortOrder.Ascending)
				{
					lvFileList.Sorting = SortOrder.Descending;
				}
				else
				{
					lvFileList.Sorting = SortOrder.Ascending;
				}
			}
			else
			{
				lvFileList.Sorting = SortOrder.Ascending;
			}
			lastSortColumn = e.Column;

			// Send the comparer the list view and column being sorted
			comparer.SortingList = lvFileList;
			comparer.Column = e.Column;

			// Attach the comparer to the list view and sort
			lvFileList.ListViewItemSorter = comparer;
			lvFileList.Sort();
		}

		/// <summary>
		/// Sets the file listing mode to No Grouping,
		/// and recounts.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		/// <remarks>FIX THIS: Add a helper to simply repopulate
		/// the list view with existing count data if it exists!!!</remarks>
		private void tsmiNoGrouping_Click(object sender, EventArgs e)
		{
			tsmiGroupByProj.Checked = false;
			tsmiGroupByType.Checked = false;

			//SumSolution();
			ReGroupFileListing();
		}

		/// <summary>
		/// Sets the file listing mode to Group by Project, 
		/// and recounts.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		/// <remarks>FIX THIS: Add a helper to simply repopulate
		/// the list view with existing count data if it exists!!!</remarks>
		private void tsmiGroupByProj_Click(object sender, EventArgs e)
		{
			tsmiGroupByType.Checked = false;
			tsmiNoGrouping.Checked = false;

			//SumSolution();
			ReGroupFileListing();
		}

		/// <summary>
		/// Sets the file listing mode to Group by Type,
		/// and recounts.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		/// <remarks>FIX THIS: Add a helper to simply repopulate
		/// the list view with existing count data if it exists!!!</remarks>
		private void tsmiGroupByType_Click(object sender, EventArgs e)
		{
			tsmiGroupByProj.Checked = false;
			tsmiNoGrouping.Checked = false;

			//SumSolution();
			ReGroupFileListing();
		}

		/// <summary>
		/// Forces a recount of all files in all projects, and
		/// updates the view.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void tsmiRecalculate_FileList_Click(object sender, EventArgs e)
		{
			ScanSolution();
			SumSolution();
		}

		/// <summary>
		/// Displays summary details for the selected project.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void tsmiViewDetailsProject_Click(object sender, EventArgs e)
		{
			if (lvSummary.SelectedItems.Count > 0)
			{
				LineCountSummary summary = (LineCountSummary)lvSummary.SelectedItems[0].Tag;
				ProjectDetails.Show(summary);
			}
		}
		#endregion

		#region Helpers
		#region Line Counting Methods
		/*
		/// <summary>
		/// Count each line in a text file, logging
		/// blank lines.
		/// </summary>
		/// <param name="info">The file information data to use.</param>
		private void CountLinesGeneric(LineCountInfo info)
		{
			StreamReader sr = new StreamReader(info.FileName);

			string line;
			while ((line = sr.ReadLine()) != null)
			{
				info.LineCountInfoDetails.Add("TotalLines", 1);
				if (line.Trim() == string.Empty)
				{
					info.LineCountInfoDetails.Add("BlankLines", 1);
				}

				info.SumMode = "Generic";
			}

			sr.Close();
		}

		/// <summary>
		/// Count each line in a c-style source file, scanning
		/// for single and multi-line comments, code, and blank lines.
		/// </summary>
		/// <param name="info">The file information data to use.</param>
		/// <remarks>This algorithm was originally created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void CountLinesCStyle(LineCountInfo info)
		{
			try
			{
				StreamReader reader = new StreamReader(info.FileName);

				string line;
				bool multiLineComment = false;
				bool hasCode = false;
				bool hasComments = false;
				while ((line = reader.ReadLine()) != null)
				{
					ParseCLine(line, ref multiLineComment, ref hasCode, ref hasComments);

					if (hasComments)
					{
						info.LineCountInfoDetails.Add("normcmtlines", 1);
					}

					if (hasCode)
					{
						info.LineCountInfoDetails.Add("codelines", 1);
					}

					if (!hasCode && !hasComments)
					{
						info.LineCountInfoDetails.Add("blanklines", 1);
					}

					info.LineCountInfoDetails.Add("totallines", 1);
				}

				reader.Close();

				info.SumMode = "C-Style";
			}
			catch
			{
			}
		}

		/// <summary>
		/// Count each line in a vb-style source file, scanning
		/// for comments, code, and blank lines.
		/// </summary>
		/// <param name="info">The file information data to use.</param>
		/// <remarks>This algorithm was originally created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void CountLinesVBStyle(LineCountInfo info)
		{
			try
			{
				StreamReader reader = new StreamReader(info.FileName);

				string line;
				bool multiLineComment = false;
				bool hasCode = false;
				bool hasComments = false;
				while ((line = reader.ReadLine()) != null)
				{
					ParseVBLine(line, ref multiLineComment, ref hasCode, ref hasComments);

					if (hasComments)
					{
						info.LineCountInfoDetails.Add("normcmtlines", 1);
					}

					if (hasCode)
					{
						info.LineCountInfoDetails.Add("codelines", 1);
					}

					if (!hasCode && !hasComments)
					{
						info.LineCountInfoDetails.Add("blanklines", 1);
					}

					info.LineCountInfoDetails.Add("totallines", 1);
				}

				reader.Close();

				info.SumMode = "Visual Basic";
			}
			catch
			{
			}
		}

		/// <summary>
		/// Count each line in an xml source file, scanning
		/// for comments, code, and blank lines.
		/// </summary>
		/// <param name="info">The file information data to use.</param>
		/// <remarks>This algorithm is based on one created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void CountLinesXMLStyle(LineCountInfo info)
		{
			try
			{
				StreamReader reader = new StreamReader(info.FileName);

				string line;
				bool multiLineComment = false;
				bool hasCode = false;
				bool hasComments = false;
				while ((line = reader.ReadLine()) != null)
				{
					ParseXMLLine(line, ref multiLineComment, ref hasCode, ref hasComments);

					if (hasComments)
					{
						info.LineCountInfoDetails.Add("normcmtlines", 1);
					}

					if (hasCode)
					{
						info.LineCountInfoDetails.Add("codelines", 1);
					}

					if (!hasCode && !hasComments)
					{
						info.LineCountInfoDetails.Add("blanklines", 1);
					}

					info.LineCountInfoDetails.Add("totallines", 1);
				}

				reader.Close();

				info.SumMode = "XML";
			}
			catch
			{
			}
		}

		/// <summary>
		/// Determines if the two input characters ch and chNext 
		/// match the given pair of characters a and b.
		/// </summary>
		/// <param name="a">First char requirement.</param>
		/// <param name="b">Second char requirement.</param>
		/// <param name="ch">First char to test.</param>
		/// <param name="chNext">Second char to test.</param>
		/// <returns></returns>
		private bool IsPair(char a, char b, char ch, char chNext)
		{
			return (ch == a && chNext == b);
		}

		/// <summary>
		/// Parses a c-style code line for comments, code, and blanks.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="multiLineComment"></param>
		/// <param name="hasCode"></param>
		/// <param name="hasComments"></param>
		/// <remarks>This algorithm was originally created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void ParseCLine(string line, ref bool multiLineComment, ref bool hasCode, ref bool hasComments)
		{
			bool inString = false;
			bool inTwoPairSequence = false;

			hasComments = multiLineComment;
			hasCode = false;

			for (int i = 0; i < line.Length; i++)
			{
				char ch = line[i];
				char chNext = (i < line.Length - 1 ? line[i + 1] : '\0');

				// Process a single-line comment
				if (IsPair('/', '/', ch, chNext) && !multiLineComment && !inString)
				{
					hasComments = true;
					return;
				}

				// Process start of a multiline comment
				else if (IsPair('/', '*', ch, chNext) && !multiLineComment && !inString)
				{
					multiLineComment = true;
					hasComments = true;
					++i;
				}

				// Process end of a multiline comment
				else if (IsPair('*', '/', ch, chNext) && !inString)
				{
					multiLineComment = false;
					++i;
				}

				// Process escaped character
				else if (ch == '\\' && !multiLineComment)
				{
					++i;
					hasCode = true;
				}

				// Process string
				else if (ch == '"' && !multiLineComment)
				{
					inString = !inString;
					hasCode = true;
				}

				else if (!multiLineComment)
				{
					if (!Char.IsWhiteSpace(ch))
					{
						hasCode = true;
					}
				}
			}
		}

		/// <summary>
		/// Parses a vb-style code line for comments, code and blanks.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="multiLineComment"></param>
		/// <param name="hasCode"></param>
		/// <param name="hasComments"></param>
		/// <remarks>This algorithm was originally created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void ParseVBLine(string line, ref bool multiLineComment, ref bool hasCode, ref bool hasComments)
		{
			bool inString = false;
			bool inTwoPairSequence = false;

			multiLineComment = false;

			line = line.Trim();

			if (line.Length == 0)
			{
				hasCode = false;
				hasComments = false;
				return;
			}

			if (line[0] == '\'')
			{
				hasCode = false;
				hasComments = true;
				return;
			}

			if (line.IndexOf('\'') != -1)
			{
				hasCode = true;
				hasComments = true;
				return;
			}

			hasCode = true;
			hasComments = true;
		}

		/// <summary>
		/// Parses an xml-style code line for comments, markup, and blanks.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="multiLineComment"></param>
		/// <param name="hasCode"></param>
		/// <param name="hasComments"></param>
		/// <remarks>This algorithm is based on one created by Oz Solomon,
		/// for his PLC line counter add-in for Visual Studio 2002/2003.</remarks>
		private void ParseXMLLine(string line, ref bool multiLineComment, ref bool hasCode, ref bool hasComments)
		{
			bool inString = false;
			bool inTwoPairSequence = false;

			hasComments = multiLineComment;
			hasCode = false;

			for (int i = 0; i < line.Length; i++)
			{
				char ch1 = line[i];
				char ch2 = (i < line.Length-1 ? line[i + 1] : '\0');
				char ch3 = (i+1 < line.Length-1 ? line[i + 2] : '\0');
				char ch4 = (i+2 < line.Length-1 ? line[i + 3] : '\0');

				// Process start of XML comment
				if (IsPair('<', '!', ch1, ch2) && IsPair('-', '-', ch3, ch4) && !multiLineComment && !inString)
				{
					multiLineComment = true;
					hasComments = true;
					i += 3;
				}

				// Process end of XML comment
				else if (IsPair('-', '-', ch1, ch2) && ch3 == '>' && !inString)
				{
					multiLineComment = false;
					i += 2;
				}

				// Process string
				else if (ch3 == '"' && !multiLineComment)
				{
					inString = !inString;
					hasCode = true;
				}

				else if (!multiLineComment)
				{
					if (!Char.IsWhiteSpace(ch1))
					{
						hasCode = true;
					}
				}
			}
		}
		*/
		#endregion

		#region Scanning and Summation Methods
		/// <summary>
		/// Scans the solution and creates a hierarchy of
		/// support objects for each project and file
		/// within each project.
		/// </summary>
		private void ScanSolution()
		{
			if (m_dte == null)
			{
				MessageBox.Show("The Visual Studio Design Time Environment was not found.", "Line Counter");
				return;
			}

			if (m_summaryList == null)
				m_summaryList = new List<LineCountSummary>();

			m_summaryList.Clear();

			Solution2 solution = (Solution2)m_dte.Solution;
			if (solution.IsOpen)
			{
				FileInfo fiSolution = new FileInfo(solution.FullName);
				LineCountSummary summary = new LineCountSummary("All Projects", m_cfgMgr.MapProjectIconIndex("{00000000-0000-0000-0000-000000000000}", imgProjectTypes) /*m_projIconMappings["{00000000-0000-0000-0000-000000000000}"]*/);
				m_summaryList.Add(summary);

				// Configure progress bars
				tsprgTotal.Minimum = 0;
				tsprgTotal.Step = 1;
				tsprgTask.Minimum = 0;
				tsprgTask.Step = 1;

				Projects projects = solution.Projects;
				tsprgTotal.Maximum = projects.Count;
				tsprgTask.Value = 0;

				ScanProjects(projects, summary);

				tsprgTask.Value = tsprgTask.Maximum;
				tsprgTotal.Value = tsprgTotal.Maximum;
			}
			else
			{
				//MessageBox.Show("There is no solution open in Visual Studio.", "Line Counter");
			}
		}

		/// <summary>
		/// Scans all of the Project objects in a 
		/// Projects collection. Properly recognizes 
		/// solution folders, and recursively processes
		/// any files, projects, and child solution 
		/// folders within.
		/// </summary>
		/// <param name="projects">The Projects collection to process.</param>
		/// <param name="allSummary">The summary object for all projects.</param>
		private void ScanProjects(Projects projects, LineCountSummary allSummary)
		{
			for (int p = 0; p < projects.Count; p++)
			{
				tsprgTotal.PerformStep();
				Project project = projects.Item(p + 1);

				Debug.WriteLine("ScanProjects():Project.Kind = " + project.Kind);
				if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder)
				{
					ScanSolutionFolder(project, allSummary);
				}
				else if (project.Kind == "{67294A52-A4F0-11D2-AA88-00C04F688DDE}")
				{
					// Unavailable project...skip
				}
				else
				{
					ScanProject(project, allSummary);
				}
			}
		}

		/// <summary>
		/// Recursively scans a solution folder item
		/// in a solution. Will handle any level of nested
		/// solution folders, and all of the projects
		/// and files within them.
		/// </summary>
		/// <param name="project">The Project object representing a solution folder.</param>
		/// <param name="allSummary">The summary object for all projects.</param>
		private void ScanSolutionFolder(Project project, LineCountSummary allSummary)
		{
			ProjectItems items = project.ProjectItems;
			tsprgTotal.Maximum += items.Count;
			for (int i = 0; i < items.Count; i++)
			{
				tsprgTotal.PerformStep();

				ProjectItem item = items.Item(i + 1);
				Debug.WriteLine("ScanSolutionFolder():ProjectItem.Kind = " + item.Kind);
				if (item.Kind == "{66A26722-8FB5-11D2-AA7E-00C04F688DDE}")
				{
					Project subProj = (Project)item.Object;
					if (subProj.Kind == "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}")
					{
						ScanSolutionFolder(subProj, allSummary);
					}
					else if (subProj.Kind == "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}")
					{
						ScanProject(subProj, allSummary);
					}
				}
				else if (item.Kind == "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}")
				{
					Project solnFolder = (Project)item.Object;
					ScanSolutionFolder(solnFolder, allSummary);
				}
			}

			ScanProjectItems(items, allSummary);
		}

		/// <summary>
		/// Scans a single Project object for all of the 
		/// countable ProjectItem objects inside.
		/// </summary>
		/// <param name="project">The Project object to scan.</param>
		/// <param name="allSummary">The summary object for all projects.</param>
		private void ScanProject(Project project, LineCountSummary allSummary)
		{
			string projName = "";
			string lang = "{00000000-0000-0000-0000-000000000000}";

			try
			{
				//if (File.Exists(project.FullName))
				//{
				if (project.FullName.IndexOf("://") != -1)
				{
					Uri uri = new Uri(project.FullName);
					projName = project.FullName;

					lang = "{00000001-0000-0000-0000-000000000000}";
				}
				else
				{
					FileInfo fiProject = new FileInfo(project.FullName);
					projName = fiProject.Name;

					CodeModel codeModel = project.CodeModel;
					lang = codeModel.Language;
				}


				LineCountSummary summary = new LineCountSummary(projName, m_cfgMgr.MapProjectIconIndex(lang, imgProjectTypes) /*m_projIconMappings[lang]*/);
				summary.ReferencedProject = project;
				m_summaryList.Add(summary);

				ProjectItems projectItems = project.ProjectItems;
				tsprgTask.Maximum = 0;
				tsprgTotal.Value = 0;
				ScanProjectItems(projectItems, summary);
				//}
			}
			catch (Exception ex)
			{
				Debug.WriteLine("ScanProject():" + ex.Message);
				Debug.WriteLine("ScanProject():" + ex.StackTrace);
			}
		}

		/// <summary>
		/// Scans the project items (files, usually) of 
		/// a project's ProjectItems collection. Recursively
		/// calls itself for nested files in the solution,
		/// such as a Form.cs/Form.Designer.cs+Form.resx 
		/// set in Visual Studio 2005.
		/// </summary>
		/// <param name="projectItems">The ProjectItems collection to scan.</param>
		/// <param name="summary">The root summary data object that these
		/// files belong to.</param>
		private void ScanProjectItems(ProjectItems projectItems, LineCountSummary summary)
		{
			if (projectItems == null)
				return;

			// {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} = Project.Kind -> C# Project
			// {E24C65DC-7377-472b-9ABA-BC803B73C61A} = Project.Kind -> Web Project

			if (projectItems.Count == 0)
				return;

			tsprgTask.Maximum += projectItems.Count;

			for (int i = 0; i < projectItems.Count; i++)
			{
				tsprgTask.PerformStep();
				ProjectItem projectItem = projectItems.Item(i + 1);

				Debug.WriteLine("ScanProjectItems():ProjectItem.Name=" + projectItem.Name);
				Debug.WriteLine("ScanProjectItems():ProjectItem.Kind=" + projectItem.Kind);

				// Skip solution folders and project nodes
				if (projectItem.Kind == "{66A26722-8FB5-11D2-AA7E-00C04F688DDE}" || projectItem.Kind == "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}")
					continue;

				// Process if item is not a directory
				if (projectItem.Kind != "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}")
				{
					for (short f = 0; f < projectItem.FileCount; f++)
					{
						try
						{
							string projectFile = projectItem.get_FileNames(f);
							if (!Directory.Exists(projectFile))
							{
								int iconIndex = m_cfgMgr.MapFileTypeIconIndex(Path.GetExtension(projectFile), imgFileTypes); //m_fileIconMappings[Path.GetExtension(projectFile)];

								LineCountInfo lineCountInfo = new LineCountInfo(projectFile, iconIndex, summary);
								summary.FileLineCountInfo.Add(lineCountInfo);
							}
						}
						catch (Exception ex)
						{
							Debug.WriteLine("ScanProjectItems():Exception:" + ex.Message);
							Debug.WriteLine("ScanProjectItems():Exception:" + ex.StackTrace);
						}
					}
				}

				ScanProjectItems(projectItem.ProjectItems, summary);
			}
		}

		/// <summary>
		/// Performs a complete counting and summation of all lines
		/// in all projects and files.
		/// </summary>
		private void SumSolution()
		{
			try
			{
				lvSummary.BeginUpdate();
				lvFileList.BeginUpdate();

				// Clean the list
				lvSummary.Items.Clear();
				lvFileList.Items.Clear();
				lvFileList.Groups.Clear();

				// Configure progress bars
				tsprgTotal.Minimum = 0;
				tsprgTotal.Step = 1;
				tsprgTask.Minimum = 0;
				tsprgTask.Step = 1;

				// Skip if there are no projects
				if (m_summaryList == null || (m_summaryList != null && m_summaryList.Count == 1))
				{
					MessageBox.Show("There are no projects loaded to summarize.", "Line Counter");
					return;
				}

				// Get all projects summary
				LineCountSummary allProjects = m_summaryList[0];
				allProjects.LineCountSummaryDetails.Reset();
				AddSummaryListItem(allProjects, lvSummary.Groups["lvgAllProj"]);

				tsprgTotal.Maximum = m_summaryList.Count;
				tsprgTotal.Value = 0;
				for (int s = 1; s < m_summaryList.Count; s++)
				{
					tsprgTotal.PerformStep();

					LineCountSummary summary = m_summaryList[s];
					summary.LineCountSummaryDetails.Reset();
					AddSummaryListItem(summary, lvSummary.Groups["lvgEachProj"]);

					tsprgTask.Maximum = summary.FileLineCountInfo.Count;
					tsprgTask.Value = 0;
					for (int i = 0; i < summary.FileLineCountInfo.Count; i++)
					{
						tsprgTask.PerformStep();

						LineCountInfo info = summary.FileLineCountInfo[i];

						// Count lines
						if (m_cfgMgr.IsFor(info.FileType, "count") /*m_countableTypes.Contains(info.FileType)*/)
						{
							info.LineCountInfoDetails.Reset();

							CountParserDelegate counter = MapCountAlgorithm(info.FileType); //m_countAlgorithms[info.FileType];
							counter(info);

							info.LineCountInfoDetails.Summarize();

							allProjects.LineCountSummaryDetails.Add(info.LineCountInfoDetails);
							summary.LineCountSummaryDetails.Add(info.LineCountInfoDetails);

							tstxtLinesCounted.Text = allProjects.LineCountSummaryDetails.TotalLines.ToString();

							AddFileListItem(info);
						}

						// Track file volume
						if (m_cfgMgr.IsFor(info.FileType, "volume"))
						{
							summary.FileVolumeDetails.Add(info.FileType, 1);
							allProjects.FileVolumeDetails.Add(info.FileType, 1);
						}
					}

					summary.LineCountSummaryDetails.Summarize();
					LineCountDetails details = summary.LineCountSummaryDetails;
					summary.LinkedListViewItem.SubItems[1].Text = details.TotalLines.ToString();
					summary.LinkedListViewItem.SubItems[2].Text = details.CodeLines.ToString();
					summary.LinkedListViewItem.SubItems[3].Text = details.CommentLines.ToString();
					summary.LinkedListViewItem.SubItems[4].Text = details.BlankLines.ToString();
					summary.LinkedListViewItem.SubItems[5].Text = details.NetLines.ToString();
					details = null;
				}

				allProjects.LineCountSummaryDetails.Summarize();
				LineCountDetails totals = allProjects.LineCountSummaryDetails;
				allProjects.LinkedListViewItem.SubItems[1].Text = totals.TotalLines.ToString();
				allProjects.LinkedListViewItem.SubItems[2].Text = totals.CodeLines.ToString();
				allProjects.LinkedListViewItem.SubItems[3].Text = totals.CommentLines.ToString();
				allProjects.LinkedListViewItem.SubItems[4].Text = totals.BlankLines.ToString();
				allProjects.LinkedListViewItem.SubItems[5].Text = totals.NetLines.ToString();

			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
				Debug.WriteLine(ex.StackTrace);
			}

			tsprgTotal.Value = tsprgTotal.Maximum;

			lvSummary.EndUpdate();
			lvFileList.EndUpdate();
		}

		private CountParserDelegate MapCountAlgorithm(string extension)
		{
			CountParserDelegate countParser = m_cfgMgr.MapCountParser("countLinesGeneric");

			string method = m_cfgMgr.AllowedMethod(extension, "count", 0);
			if (method != null)
			{
				CountParserDelegate tempParser = m_cfgMgr.MapCountParser(method);
				if (tempParser != null)
					countParser = tempParser;
			}

			return countParser;
		}

		/// <summary>
		/// Adds a summary item to the projects list view.
		/// </summary>
		/// <param name="summary">The summary data object to reference.</param>
		/// <param name="group">The summary list view group this item
		/// should be listed under.</param>
		private void AddSummaryListItem(LineCountSummary summary, ListViewGroup group)
		{
			ListViewItem lvi = new ListViewItem();
			lvi.Text = summary.ProjectName;
			lvi.SubItems.Add("0");
			lvi.SubItems.Add("0");
			lvi.SubItems.Add("0");
			lvi.SubItems.Add("0");
			lvi.SubItems.Add("0");

			lvi.Tag = summary;
			lvi.ImageIndex = summary.IconIndex;
			//lvi.StateImageIndex = summary.IconIndex;
			lvi.Group = group;

			summary.LinkedListViewItem = lvi;

			lvSummary.Items.Add(lvi);
		}

		/// <summary>
		/// Adds a file information item to the file list view.
		/// </summary>
		/// <param name="info">The file information data object.</param>
		private void AddFileListItem(LineCountInfo info)
		{
			FileInfo fileInfo = new FileInfo(info.FileName);

			ListViewItem lvi = new ListViewItem();
			lvi.Text = fileInfo.Name;
			lvi.SubItems.Add(info.LineCountInfoDetails.TotalLines.ToString());
			lvi.SubItems.Add(info.LineCountInfoDetails.CodeLines.ToString());
			lvi.SubItems.Add(info.LineCountInfoDetails.CommentLines.ToString());
			lvi.SubItems.Add(info.FileType);
			lvi.SubItems.Add(info.SumMode);

			lvi.Tag = info;

			// Add image index setting
			int iconIndex = info.IconIndex;// m_cfgMgr.MapProjectIconIndex(info.FileType, imgFileTypes); //m_fileIconMappings[info.FileType];
			
			lvi.ImageIndex = iconIndex;
			//lvi.StateImageIndex = iconIndex;


			if (tsmiGroupByType.Checked)
			{
				ListViewGroup group = lvFileList.Groups["groupType" + info.FileType.Substring(1)];
				if (group == null)
				{
					group = new ListViewGroup("groupType" + info.FileType.Substring(1), info.FileType.Substring(1).ToUpper() + " Files");
					lvFileList.Groups.Add(group);
				}

				lvi.Group = group;
			}
			else if (tsmiGroupByProj.Checked)
			{
				ListViewGroup group = lvFileList.Groups["groupProj" + info.ProjectSummary.ProjectName];
				if (group == null)
				{
					group = new ListViewGroup("groupProj" + info.ProjectSummary.ProjectName, info.ProjectSummary.ProjectName + " Files");
					lvFileList.Groups.Add(group);
				}

				lvi.Group = group;
			}

			lvFileList.Items.Add(lvi);
		}

		private void GatherTMVStatistics()
		{
		}

		private void ReGroupFileListing()
		{
			lvFileList.Groups.Clear();
			lvFileList.BeginUpdate();
			for (int i = 0; i < lvFileList.Items.Count; i++)
			{
				ListViewItem lvi = lvFileList.Items[i];
				LineCountInfo info = (LineCountInfo)lvi.Tag;

				if (tsmiGroupByType.Checked)
				{
					ListViewGroup group = lvFileList.Groups["groupType" + info.FileType.Substring(1)];
					if (group == null)
					{
						group = new ListViewGroup("groupType" + info.FileType.Substring(1), info.FileType.Substring(1).ToUpper() + " Files");
						lvFileList.Groups.Add(group);
					}

					lvi.Group = group;
				}
				else if (tsmiGroupByProj.Checked)
				{
					ListViewGroup group = lvFileList.Groups["groupProj" + info.ProjectSummary.ProjectName];
					if (group == null)
					{
						group = new ListViewGroup("groupProj" + info.ProjectSummary.ProjectName, info.ProjectSummary.ProjectName + " Files");
						lvFileList.Groups.Add(group);
					}

					lvi.Group = group;
				}
			}
			lvFileList.EndUpdate();
		}
		#endregion
		#endregion

		private void lvSummary_DoubleClick(object sender, EventArgs e)
		{
			
		}

		private void lvSummary_ItemActivate(object sender, EventArgs e)
		{
			tsmiViewDetailsProject_Click(sender, e);
		}

		private void tsmiTypeMemberVolume_Click(object sender, EventArgs e)
		{

		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Jon Rista has been programming since the age of 8 (first Pascal program), and has been a programmer since the age of 10 (first practical program). In the last 21 years, he has learned to love C++, embrace object orientation, and finally enjoy the freedom of C#. He knows over 10 programming languages, and vows that his most important skill in programming is creativity, even more so than logic. Jon works on large-scale enterprise systems design and implementation, and employs Design Patterns, C#, .NET, and SQL Server in his daily doings.

Comments and Discussions