none
Format Document VS 2012 - HTML

    Question

  • Recently moved to vs 2012.  Normally in 2010 I could Edit->Advanced->Format Document or use Ctrl-E,D on an Html document and it would cleanup the code and remove blank lines.  Now in 2012 when this is ran, it no longer removes blank lines, really it just indents.  How can I get 2012 to remove blank lines like it once did in 2010?
    Wednesday, September 19, 2012 1:10 PM

Answers

  • Well, it turned out to be more than a handful, but a lot of it is just boilerplate.  I ended up implementing the extension because I wasn't sure how to approach the editor specific line removal piece.  Instructions are below

    1:  Download the VS SDK if you don't have it (here).

    2:  Create a new project using the C# -> Extensibility -> Text Editor Adornment template (we aren't making a text editor adornment, but the project sets up a bunch of plumbing around deployment, MEF contributions, etc...)

    3:  Delete the two files the SDK adds, or rename them. I chose to have two files named EditorViewFilter.cs and TextViewCreationListener.cs.  If you keep the existing files and just rename them, make sure you delete their contents entirely.

    4:  Add the following references to the project:  Microsoft.VisualStudio.Editor, Microsoft.VisualStudio.TextManager.Interop, Microsoft.VisualStudio.Ole.Interop, Microsoft.VisualStudio.Shell.11.0

    5:  Add this code to the EditorViewFilter.cs file

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Formatting;
    using Microsoft.VisualStudio.TextManager.Interop;
    using System;
    using System.Linq;
    
    namespace HTMLFormatter
    {
        public class EditorViewFilter : IOleCommandTarget
        {
            public IOleCommandTarget NextCommandTarget { get; set; }
    
            private IVsTextView TextView { get; set; }
            private IVsEditorAdaptersFactoryService EditorAdapterFactoryService { get; set; }
    
            public EditorViewFilter(IVsTextView textView, IVsEditorAdaptersFactoryService editorAdapterFactoryService)
            {
                TextView = textView;
                EditorAdapterFactoryService = editorAdapterFactoryService;
            }
    
            /// <summary>
            /// Handles command execution requests in VS, we simply want to watch for the FORMATDOCUMENT command, forward it on to the real handler and then, afterwards, make another pass to remove
            /// empty lines.
            /// </summary>
            public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
            {
                int res = (int)Constants.OLECMDERR_E_NOTSUPPORTED;
                if (NextCommandTarget != null)
                {
                    //Forward the command to the next handler so it gets to the person that will eventually be doing the real formatting.
                    res = NextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
                }
    
                if (pguidCmdGroup == VSConstants.CMDSETID.StandardCommandSet2K_guid)
                {
                    if (nCmdID == (uint)VSConstants.VSStd2KCmdID.FORMATDOCUMENT)
                    {
                        if (TextView != null)
                        {
                            IWpfTextView wpfTextView = EditorAdapterFactoryService.GetWpfTextView(TextView);
    
                            if (wpfTextView != null)
                            {
                                //We will want to delete them in reverse order so that removal of prior lines won't screw with the absolute buffer position values
                                //for latter lines.
                                var linesToRemove = wpfTextView.TextViewLines.Where(ShouldDeleteLine).Reverse();
    
                                var textBuffer = wpfTextView.TextBuffer;
                                foreach (var line in linesToRemove)
                                {
                                    textBuffer.Delete(new Span(line.Start.Position, line.LengthIncludingLineBreak));
                                }
                            }
                        }
                    }
                }
    
                return res;
            }
    
            /// <summary>
            /// Handles determining if commands in VS are enabled/visible/etc... We don't want to muck with anything here so we always forward every command along the route without doing anything.
            /// </summary>
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
            {
                if (NextCommandTarget != null)
                {
                    //Forward the command to the next handler in the chain.
                    return NextCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
                }
    
                return (int)Constants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            private static bool ShouldDeleteLine(ITextViewLine line)
            {
                return String.IsNullOrWhiteSpace(line.Snapshot.GetText(line.Start.Position, line.LengthIncludingLineBreak));
            }
        }
    }

    6: Add this code to the TextViewCreationListener.cs file

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.VisualStudio.Utilities;
    using System.ComponentModel.Composition;
    
    namespace HTMLFormatter
    {
        //Exports a listener for text view creations in VS, specifically for all text views whose content type is HTML. This is called when such a view is created, we use this as a hook point to attach
        //a command filter (via AddCommandFilter) in order to 'sniff' command execution requests.
        [Export(typeof(IVsTextViewCreationListener))]
        [ContentType("HTML")]
        [TextViewRole(PredefinedTextViewRoles.Interactive)]
        class TextViewCreationListener : IVsTextViewCreationListener
        {
            [Import]
            private IVsEditorAdaptersFactoryService EditorAdapterFactory { get; set; }
    
            public void VsTextViewCreated(IVsTextView textViewAdapter)
            {
                IOleCommandTarget forwardingTarget;
                EditorViewFilter filter = new EditorViewFilter(textViewAdapter, EditorAdapterFactory);
                if (ErrorHandler.Succeeded(textViewAdapter.AddCommandFilter(filter, out forwardingTarget)))
                {
                    //Store the next target in the chain. AddCommandFilter adds to a LIFO structure, so we are responsible for passing requests 'down the line' if we don't 
                    //handle them ourselves. To do that we need the previous head of the chain, which is returned from AddCommandFilter.
                    filter.NextCommandTarget = forwardingTarget;
                }
            }
        }
    }

    7: Hit F5, this will launch the Experimental Instance of VS (so you have to choose your profile, just like on VS first launch). This is done so the extension doesn't 'pollute' your primary instance during development.

    8:  Open a project with an HTML file. I made a new MVC4 app, I am not a web dev so I am not really familar with all the options here, but this one has HTML (like) files, and it worked for my testing :)

    9:  Invoke Edit->Advanced->Format Document

    10:  If all goes as it did for me (and I suspect it should) the document should have the normal formatting applied that the built in Format Document does, as well as any blank lines stripped out.

    11:  Once happy with functionality if you look in the projects output directory (bin\<build flavor>) there should be a file with a .vsix extension. Double clicking on that will bring up a dialog about installing the extension to VS, after doing that it will be installed in your 'real' VS instance (not just the Experimental instance, which is where the project deploys it to).

    Feel free to ask questions here, I tried to add some commenting to the code, but if you are unfamilar with general VS extensibility it may not make much sense :)

    The only remaining item I can imagine (ignoring bug fixes if/when they are found) would be to create a linked undo transaction so that all of the line removals can be undone/redone as a single action. The built in formatting command handler does this, but you can't 'get involved' in that transaction so your line removals will always be a seperate undo/redo action. Right now, since I didn't try to integrate into the undo/redo system each line removal is a unique undo/redo action, as that is the default if you don't try and do anything else.


    Ryan


    Thursday, September 20, 2012 7:32 PM

All replies

  • According to someone on the HTML team:

    "This was intentional. We’ve had a lot of complaints that we removed blank lines, because people use it a lot to organize their HTML documents."

    It doesn't sound like there is a switch to get the old behavior back.

    Ryan

    Wednesday, September 19, 2012 11:39 PM
  • I was afraid it was something like that.  I personally thought that was a great feature.  It would be nice to have it as an option to toggle.

    Jason

    Thursday, September 20, 2012 1:17 PM
  • I suspect it would be fairly trivial to write it in as an extension. One that adds a command filter to every HTML view and after doing the real Exec of the command (to get the normal formatting done) simply scans through the buffer and removes all blank lines. Of course 'trivial' implies both some familiarity with VS extensibility and of course usage of a non-Express SKU since Express can't have extensions installed. I could outline the general approach if you are interested, I can't see it being more than a handful of lines of code.

    Ryan

    Thursday, September 20, 2012 5:20 PM
  • I've never written an extension, but I would gladly take your general outline, might be interesting to do.

    Jason

    Thursday, September 20, 2012 5:59 PM
  • Well, it turned out to be more than a handful, but a lot of it is just boilerplate.  I ended up implementing the extension because I wasn't sure how to approach the editor specific line removal piece.  Instructions are below

    1:  Download the VS SDK if you don't have it (here).

    2:  Create a new project using the C# -> Extensibility -> Text Editor Adornment template (we aren't making a text editor adornment, but the project sets up a bunch of plumbing around deployment, MEF contributions, etc...)

    3:  Delete the two files the SDK adds, or rename them. I chose to have two files named EditorViewFilter.cs and TextViewCreationListener.cs.  If you keep the existing files and just rename them, make sure you delete their contents entirely.

    4:  Add the following references to the project:  Microsoft.VisualStudio.Editor, Microsoft.VisualStudio.TextManager.Interop, Microsoft.VisualStudio.Ole.Interop, Microsoft.VisualStudio.Shell.11.0

    5:  Add this code to the EditorViewFilter.cs file

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Formatting;
    using Microsoft.VisualStudio.TextManager.Interop;
    using System;
    using System.Linq;
    
    namespace HTMLFormatter
    {
        public class EditorViewFilter : IOleCommandTarget
        {
            public IOleCommandTarget NextCommandTarget { get; set; }
    
            private IVsTextView TextView { get; set; }
            private IVsEditorAdaptersFactoryService EditorAdapterFactoryService { get; set; }
    
            public EditorViewFilter(IVsTextView textView, IVsEditorAdaptersFactoryService editorAdapterFactoryService)
            {
                TextView = textView;
                EditorAdapterFactoryService = editorAdapterFactoryService;
            }
    
            /// <summary>
            /// Handles command execution requests in VS, we simply want to watch for the FORMATDOCUMENT command, forward it on to the real handler and then, afterwards, make another pass to remove
            /// empty lines.
            /// </summary>
            public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
            {
                int res = (int)Constants.OLECMDERR_E_NOTSUPPORTED;
                if (NextCommandTarget != null)
                {
                    //Forward the command to the next handler so it gets to the person that will eventually be doing the real formatting.
                    res = NextCommandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
                }
    
                if (pguidCmdGroup == VSConstants.CMDSETID.StandardCommandSet2K_guid)
                {
                    if (nCmdID == (uint)VSConstants.VSStd2KCmdID.FORMATDOCUMENT)
                    {
                        if (TextView != null)
                        {
                            IWpfTextView wpfTextView = EditorAdapterFactoryService.GetWpfTextView(TextView);
    
                            if (wpfTextView != null)
                            {
                                //We will want to delete them in reverse order so that removal of prior lines won't screw with the absolute buffer position values
                                //for latter lines.
                                var linesToRemove = wpfTextView.TextViewLines.Where(ShouldDeleteLine).Reverse();
    
                                var textBuffer = wpfTextView.TextBuffer;
                                foreach (var line in linesToRemove)
                                {
                                    textBuffer.Delete(new Span(line.Start.Position, line.LengthIncludingLineBreak));
                                }
                            }
                        }
                    }
                }
    
                return res;
            }
    
            /// <summary>
            /// Handles determining if commands in VS are enabled/visible/etc... We don't want to muck with anything here so we always forward every command along the route without doing anything.
            /// </summary>
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
            {
                if (NextCommandTarget != null)
                {
                    //Forward the command to the next handler in the chain.
                    return NextCommandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
                }
    
                return (int)Constants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            private static bool ShouldDeleteLine(ITextViewLine line)
            {
                return String.IsNullOrWhiteSpace(line.Snapshot.GetText(line.Start.Position, line.LengthIncludingLineBreak));
            }
        }
    }

    6: Add this code to the TextViewCreationListener.cs file

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.VisualStudio.Utilities;
    using System.ComponentModel.Composition;
    
    namespace HTMLFormatter
    {
        //Exports a listener for text view creations in VS, specifically for all text views whose content type is HTML. This is called when such a view is created, we use this as a hook point to attach
        //a command filter (via AddCommandFilter) in order to 'sniff' command execution requests.
        [Export(typeof(IVsTextViewCreationListener))]
        [ContentType("HTML")]
        [TextViewRole(PredefinedTextViewRoles.Interactive)]
        class TextViewCreationListener : IVsTextViewCreationListener
        {
            [Import]
            private IVsEditorAdaptersFactoryService EditorAdapterFactory { get; set; }
    
            public void VsTextViewCreated(IVsTextView textViewAdapter)
            {
                IOleCommandTarget forwardingTarget;
                EditorViewFilter filter = new EditorViewFilter(textViewAdapter, EditorAdapterFactory);
                if (ErrorHandler.Succeeded(textViewAdapter.AddCommandFilter(filter, out forwardingTarget)))
                {
                    //Store the next target in the chain. AddCommandFilter adds to a LIFO structure, so we are responsible for passing requests 'down the line' if we don't 
                    //handle them ourselves. To do that we need the previous head of the chain, which is returned from AddCommandFilter.
                    filter.NextCommandTarget = forwardingTarget;
                }
            }
        }
    }

    7: Hit F5, this will launch the Experimental Instance of VS (so you have to choose your profile, just like on VS first launch). This is done so the extension doesn't 'pollute' your primary instance during development.

    8:  Open a project with an HTML file. I made a new MVC4 app, I am not a web dev so I am not really familar with all the options here, but this one has HTML (like) files, and it worked for my testing :)

    9:  Invoke Edit->Advanced->Format Document

    10:  If all goes as it did for me (and I suspect it should) the document should have the normal formatting applied that the built in Format Document does, as well as any blank lines stripped out.

    11:  Once happy with functionality if you look in the projects output directory (bin\<build flavor>) there should be a file with a .vsix extension. Double clicking on that will bring up a dialog about installing the extension to VS, after doing that it will be installed in your 'real' VS instance (not just the Experimental instance, which is where the project deploys it to).

    Feel free to ask questions here, I tried to add some commenting to the code, but if you are unfamilar with general VS extensibility it may not make much sense :)

    The only remaining item I can imagine (ignoring bug fixes if/when they are found) would be to create a linked undo transaction so that all of the line removals can be undone/redone as a single action. The built in formatting command handler does this, but you can't 'get involved' in that transaction so your line removals will always be a seperate undo/redo action. Right now, since I didn't try to integrate into the undo/redo system each line removal is a unique undo/redo action, as that is the default if you don't try and do anything else.


    Ryan


    Thursday, September 20, 2012 7:32 PM
  • Wow - talk about above and beyond, I'm excited to experiment with this over the weekend.  Thanks!

    Jason

    Thursday, September 20, 2012 8:27 PM