2011-10-07

Continuing my last post on creating a basic history extension, let’s actually do something! Here, we dive into capturing events to code documents in Visual Studio and using that to create a help build a local code history for edits.

First, let’s make a new SaveListener class that will subscribe to Running Document Table events.

class SaveListener : IVsRunningDocTableEvents3 {

We need to request the SVsRunningDocumentTable service and subscribe to it’s events.

IVsRunningDocumentTable m_RDT;
uint m_rdtCookie = 0;

public bool Register()
{
   // Register events for running document table.
   m_RDT = (IVsRunningDocumentTable)Package.GetGlobalService(typeof(SVsRunningDocumentTable));
   m_RDT.AdviseRunningDocTableEvents(this, out m_rdtCookie);

   return true;
}

Because SaveListener implements the IVsRunningDocTableEvents3 interface, we’ll able to pass this to receive event notifications.

For this blog post, let’s try to grab and cache a copy of files about to be saved. This snippet will let us get some information about the file about to be saved such as the file name:

public int OnBeforeSave(uint docCookie)
{
   uint flags, readlocks, editlocks;
   string name; IVsHierarchy hier;
   uint itemid; IntPtr docData;
   m_RDT.GetDocumentInfo(docCookie, out flags, out readlocks, out editlocks, out name, out hier, out itemid, out docData);

   CopyFileToCache(name);

   return VSConstants.S_OK;
}

Here is some C# IO code that will take care of copying the file to a cache directory:

public void CopyFileToCache(string file)
{
   try
   {
      var newPath = PrepareDocumentCache(file, DateTime.Now);
      System.IO.File.Copy(file, newPath, true);
   }
   catch (Exception ex)
   {
     Trace.WriteLine(ex.Message);
   }
}

private string PrepareDocumentCache(string file, DateTime day)
{
   string relativePath = file.Replace(SolutionBaseDirectory + "\\", "");
   var dayStr = MakeDayString(day);
   var time = MakeTimeString(day);
   var newPath = System.IO.Path.Combine(ContextRepository, dayStr, relativePath + "$" + time);
   var dirPath = System.IO.Path.GetDirectoryName(newPath);
   if (!System.IO.File.Exists(dirPath))
   {
      // This will create subdirectories too if missing...
      System.IO.Directory.CreateDirectory(dirPath);
   }
   return newPath;
}

private static string MakeDayString(DateTime day)
{
   return string.Format("{0:0000}", day.Year) + "\\" + string.Format("{0:00}", day.Month) + "\\" + string.Format("{0:00}", day.Day);
}

private static string MakeTimeString(DateTime time)
{
   return time.ToString("hh.mm.ss.fff.tt");
}

Let’s add some code to shutdown the save listener.

public void Shutdown()
{
   if (m_RDT != null)
   {
      m_RDT.UnadviseRunningDocTableEvents(m_rdtCookie);
      m_RDT = null;
   }
}

Finally, let’s hook up the save listener from our Package we created in the first post so we can start it up and shut it down at the right time:

SaveListener m_saveListener;
public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
{
   m_dte = (EnvDTE.DTE)this.GetService(typeof(EnvDTE.DTE));
   if (m_dte == null)
      ErrorHandler.ThrowOnFailure(1);
   if( m_dte.Solution != null )
   {
      Trace.WriteLine(m_dte.Solution.FullName);
      m_saveListener = new SaveListener();
      m_saveListener.SolutionBaseDirectory = System.IO.Path.GetDirectoryName(m_dte.Solution.FullName);
      m_saveListener.ContextRepository = ".codeHistory";
      m_saveListener.Register();
   }
   return VSConstants.S_OK;
}

Ok! That’s the basic idea behind creating a local history extension. Here, we’re just copying the file to a cache. Eclipse does something similar, except they use a heap directory structure. I even have having each save be a commit to git, and it works nicely!

For some applications of a local history repository, check out my earlier post of “code diffs redesigned”. Another idea is to try out some developer analytics. What API calls was I spending all my time mucking around with? Maybe there is a lesson to be learned there?



blog comments powered by Disqus