View unanswered posts | View active topics



Reply to topic  [ 23 posts ]  Go to page 1, 2  Next
some auto-indenting goodness 
Author Message
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
The following changes integrate a smart paste, with auto-indent of the pasted text to the correct level at the destination. Also, the ability to move lines up or down, adjusting the indentation accordingly.

There's some nifty things you can do with the latter, including moving the start or end of control blocks ('{' or '}'), and it reindents the code moving in and out of the different levels accordingly. (Think of it like a dynamic version of "surround with"). If you use the 'BracesAfterLine' style of code formatting, it picks up the open brace and the line above it together, when using the Alt-Up/Down from either position.

The auto-indentation works with HTML/XML as well.

Note that the FD4 code base recently added the MoveLineUp() and MoveLineDown() from FDjpPlugin, but those routines have a bug where if you select full lines via the left margin, it moves one too many lines. The version below fixes that problem, along with consolidating the two functions into one, and adding the reindentation. If you choose to use these, they need to replace the existing versions.

I'd appreciate any feedback on the usefulness, and correct handling of your particular coding style.

For example, FD appears to be set up to have the code after 'case' statements at the same level as the case, whereas I personally use one extra indent level here. The ReindentLines() function needs to behave differently in these scenarios, so I'm hoping that a configuration setting can also be added to control this. (I didn't know how to do that). You'll see TODO notes where that test would be used.

If such a configuration is added for "indent after case:", there might also be a way to use that setting in the snippet for the 'switch', similar to how the $(CSLB) works. It's not that hard to tweak the snippet by hand, but if it can automatically track the setting, that would sure be ideal.

Okay, so here are the code changes:

In MainForm.cs
Code:
        /// <summary>
        /// Pastes lines at the correct indent level
        /// </summary>
        public void SmartPaste(Object sender, System.EventArgs e)
        {
            ScintillaControl sci = Globals.SciControl;
            if (sci.CanPaste)
            {
                if (!Clipboard.GetText().EndsWith("\n") || Clipboard.ContainsData("MSDEVColumnSelect"))
                {
                    // if clip is not line-based, then just do simple paste
                    sci.Paste();
                }
                else
                {
                    sci.BeginUndoAction();
                    sci.Home();
                    int pos = sci.CurrentPos;
                    int lines = sci.LineCount;
                    sci.Paste();
                    lines = sci.LineCount - lines;      // = # of lines in the pasted text
                    sci.ReindentLines(sci.LineFromPosition(pos), lines);
                    sci.EndUndoAction();
                }
            }
        }

        // This code is taken from FDjpPlugin, fixed, shortened, and adapted with re-indenting
        private void MoveLine(int dir)
        {
            ScintillaControl sci = Globals.SciControl;
            int start = sci.SelectionStart < sci.SelectionEnd ? sci.SelectionStart : sci.SelectionEnd;
            int end = sci.SelectionStart > sci.SelectionEnd ? sci.SelectionStart : sci.SelectionEnd;
            int startLine = sci.LineFromPosition(start);
            int endLine = sci.LineFromPosition(end);
            if (sci.PositionFromLine(endLine) != end || startLine == endLine)
            {
                ++endLine;      // selection was not made in whole lines, so extend the end of selection to the start of the next line
            }
            if (sci.SelectionStart == sci.SelectionEnd && PluginBase.MainForm.Settings.CodingStyle == CodingStyle.BracesAfterLine)
            {
                // TODO? do we need a better test for moving open brace and line above it together
                if (sci.GetLine(startLine).Trim().StartsWith("{"))
                {
                    --startLine;
                }
                else if (sci.GetLine(startLine + 1).Trim().StartsWith("{"))
                {
                    ++endLine;
                }
            }
            int len = endLine - startLine;
            sci.BeginUndoAction();
            sci.SelectionStart = sci.PositionFromLine(startLine);
            sci.SelectionEnd = sci.PositionFromLine(endLine);
            string selectStr = sci.SelText;
            sci.Clear();
            if (dir > 0)
            {
                sci.LineDown();
            }
            else
            {
                sci.LineUp();
            }
            startLine += dir;
            if (startLine == 0 || startLine > sci.LineCount)
            {
                startLine -= dir;   // line # moved past limits, so back out the change
            }
            else
            {
                int ctrlBlock = 0;
                if (len <= 2)
                {
                    ctrlBlock = sci.IsControlBlock(selectStr);
                }
                if (ctrlBlock != 0)      // if we're moving a single control block start/end, reindent the affected lines that are moving in or out of the block
                {
                    int line = startLine;
                    if (dir > 0)
                    {
                        --line;
                    }
                    int indent = dir * sci.Indent;
                    if (ctrlBlock < 0)
                    {
                        indent = -indent;
                    }
                    sci.SetLineIndentation(line, sci.GetLineIndentation(line) + indent);
                }
            }
            start = sci.PositionFromLine(startLine);
            sci.InsertText(start, selectStr);
            sci.ReindentLines(startLine, len);
            sci.SelectionStart = start;
            sci.SelectionEnd = sci.PositionFromLine(startLine + len);
            sci.EndUndoAction();
        }

       /// <summary>
        /// Move the current line (or selected lines) up.
        /// </summary>
        public void MoveLineUp(Object sender, System.EventArgs e)
        {
            MoveLine(-1);
        }

        /// <summary>
        /// Move the current line (or selected lines) down.
        /// </summary>
        public void MoveLineDown(Object sender, System.EventArgs e)
        {
            MoveLine(1);
        }


In ScintillaControl.cs (I put it above IndentLine)
Code:
        /// <summary>
        /// Determines whether the input string is a start/end of a control block
        /// Returns -1:start, 1:end, 0:neither
        /// <!summary>
        public int IsControlBlock(string str)
        {
            int ret = 0;
            str = str.Trim();
            // TODO? is there a lexer test for "start/end of control block"?
            // For now, do a couple of manual language tests
            if (ConfigurationLanguage == "xml" || ConfigurationLanguage == "html" || ConfigurationLanguage == "css")
            {
                if (str.StartsWith("</"))
                {
                    ret = 1;
                }
                else if (str.IndexOf('/') < 0)      // TODO this should be a more robust test for a block start
                {
                    ret = -1;
                }
            }
            else
            {
                if (str[0] == '}')
                {
                    ret = 1;
                }
                else if (CodeEndsWith(str, "{"))
                {
                    ret = -1;
                }
            }
            return ret;
        }

        /// <summary>
        /// Tests whether the code-portion of a string (i.e. comments trimmed) ends with a string value
        /// <!summary>
        public bool CodeEndsWith(string str, string value)
        {
            bool ret = false;
            int startIndex = str.LastIndexOf(value);
            if (startIndex >= 0)
            {
                String lineComment = Configuration.GetLanguage(ConfigurationLanguage).linecomment;
                if (lineComment != "")
                {
                    int slashIndex = str.LastIndexOf(lineComment);
                    if (slashIndex >= startIndex)           // if there's a comment after the sequence we're looking for...
                    {
                        str = str.Substring(0, slashIndex); // ...remove it
                    }
                }
                if (str.Trim().EndsWith(value))
                {
                    ret = true;
                }
            }
            return ret;
        }

        /// <summary>
        /// Returns the first line of a string
        /// <!summary>
        public string FirstLine(string str)
        {
            char newline = (EOLMode == 1) ? '\r' : '\n';
            int eol = str.IndexOf(newline);
            if (eol < 0)
            {
                return str;
            }
            else
            {
                return str.Substring(0, eol);
            }
        }

        /// <summary>
        /// Reindents a block of pasted or moved lines to match the indentation of the destination pos
        /// </summary>
        public void ReindentLines(int startLine, int nLines)
        {
            if (nLines <= 0)
            {
                return;
            }
            String lineComment = Configuration.GetLanguage(ConfigurationLanguage).linecomment;
            String pasteStr = "";
            String destStr = "";
            int commentIndent = -1;
            int pasteIndent = -1;
            int line;
            // scan pasted lines to find their indentation
            for (line = startLine; line < startLine + nLines; ++line)
            {
                pasteStr = GetLine(line).Trim();
                if (pasteStr != "")
                {
                    if (lineComment != "" && pasteStr.StartsWith(lineComment))
                    {
                        if (commentIndent < 0)
                        {
                            commentIndent = GetLineIndentation(line);   // indent of the first commented line
                        }
                    }
                    else
                    {
                        commentIndent = -1;     // we found code, so we won't be using comment-based indenting
                        pasteIndent = GetLineIndentation(line);
                        break;
                    }
                }
            }
            // now scan the destination to determine its indentation
            int destIndent = -1;
            for (line = startLine; --line >= 1; )
            {
                destStr = GetLine(line).Trim();
                if (destStr != "")
                {
                    if (pasteIndent < 0)
                    {
                        // no code lines were found in the paste, so use the comment indentation
                        pasteIndent = commentIndent;
                        destIndent = GetLineIndentation(line);  // destination indent at any non-blank line
                        break;
                    }
                    else
                    {
                        if (lineComment == "" || !destStr.StartsWith(lineComment))
                        {
                            destIndent = GetLineIndentation(line);  // destination indent at first code-line
                            if (IsControlBlock(destStr) < 0)
                            {
                                // indent when we're pasting at the start of a control block (unless we're pasting an end block),
                                if (IsControlBlock(pasteStr) <= 0)
                                {
                                    destIndent += Indent;
                                }
                            }
                            else
                            {
                                // outdent when we're pasting the end of a control block anywhere but after the start of a control block
                                if (IsControlBlock(pasteStr) > 0)
                                {
                                    destIndent -= Indent;
                                }
                            }
                            if (true)  // TODO should test a config value for indenting after "case:" statements
                            {
                                if (CodeEndsWith(destStr, ":") && !CodeEndsWith(FirstLine(pasteStr), ":"))
                                {
                                    // if dest line ends with ":" and paste line doesn't
                                    destIndent += Indent;
                                }
                                if (CodeEndsWith(FirstLine(pasteStr), ":") && CodeEndsWith(destStr, ";"))
                                {
                                    // if paste line ends with ':' and dest line doesn't
                                    destIndent -= Indent;
                                }
                            }
                            break;
                        }
                    }
                }
            }
            if (pasteIndent < 0)
            {
                pasteIndent = 0;
            }
            if (destIndent < 0)
            {
                destIndent = 0;
            }
            while (--nLines >= 0)
            {
                int indent = GetLineIndentation(startLine);
                if (indent != 0) // TODO? || (startIndent == 0 && not a comment, or preprocessor line)
                {
                    SetLineIndentation(startLine, destIndent + indent - pasteIndent);
                }
                ++startLine;
            }
        }


And finally, this will add the as-you-type indenting for "case:" statements, if you prefer that style of indenting after the case. This goes in ScintillaControl.cs, in the function OnSmartIndent(), case Enums.SmartIndent.CPP:. The first several lines and the last line give some placement context, and the new code starts at the 'TODO' comment. As the note says, it should probably be paired with a configuration variable to control this feature.
Code:
                            if (tempText.EndsWith("{"))
                            {
                                int bracePos = CurrentPos - 1;
                                while (bracePos > 0 && CharAt(bracePos) != '{') bracePos--;
                                if (bracePos >= 0 && CharAt(bracePos) == '{' && BaseStyleAt(bracePos) == 10)
                                    previousIndent += Indent;
                            }
                            // TODO this should test a config variable for indenting after case : statements
                            if (true && tempText.EndsWith(":"))
                            {
                                int prevLine = tempLine;
                                while (--prevLine > 0)
                                {
                                    tempText = GetLine(prevLine).Trim();
                                    if (tempText.Length != 0 && !tempText.StartsWith("//"))
                                    {
                                        int prevIndent = GetLineIndentation(prevLine);
                                        if ((tempText.EndsWith(";") && previousIndent == prevIndent) ||
                                            (tempText.EndsWith(":") && previousIndent == prevIndent + Indent))
                                        {
                                            previousIndent -= Indent;
                                            SetLineIndentation(tempLine, previousIndent);
                                        }
                                        break;
                                    }
                                }
                                previousIndent += Indent;
                            }
                            IndentLine(curLine, previousIndent);


Thu Jan 20, 2011 6:59 pm
Profile
Admin

Joined: Tue Aug 30, 2005 6:14 pm
Posts: 3043
Location: Finland
Reply with quote
Commited with small mods, could you check that i didn't break anything? :)


Thu Jan 20, 2011 9:58 pm
Profile WWW
Member

Joined: Mon Apr 12, 2010 10:17 pm
Posts: 114
Reply with quote
Ummm no... I disagree John, I'd be more inclined to say GREATNESS!
Just what the doctor ordered, so many times I've wanted all these.

Just one thing (sorry only had time to try them for 5 mins or so)
SmartPaste- will only work if selected from margin,
ie it's not picking up the EOL chars & not getting past the ClipBoard.EndsWith('\n'); test if you just make a free selection,
or should I say you have to select to next line to pick up the EOL chars(& not \t's of course).
Possible to fix? or am I just not doing something right?

Luv them all, thanks for the contribution, some more VS goodness inserted to FD...

btw I just inserted them all to ScintillaControl, instead of some in MainForm, any reason to took that approach?
edit:Oh, didn't see you included them already Mika,must've had this thread open a while lol, thanx for that!


Thu Jan 20, 2011 10:23 pm
Profile
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
Mika, looks like everything works great -- thanks!

I've reviewed the differences so that I can hopefully make future submissions that require less editing. Most of them were style differences that I can easily match. But I didn't understand the need for the various "this." additions, and how to know which methods require that type of access, and which don't. If that's easy to explain, please feel free to do so.

jjase: Thanks for the comments. Much appreciated. :)

The intention on SmartPaste, is that if the user only copied a partial line (even if that selection extends to multiple lines), then it loses some of the significance of being "whole lines of code". One of the other things that SmartPaste does, is always paste into column 1, regardless of where the cursor is on the line. I find that handy myself, but when selections are copied from mid-line, it increases the chances that the user might want to paste at a specific position as well.

If all that makes sense, and you still think that there are unambiguous cases that could be handled better, could you expand on the details just a bit and we can evaluate them?


Fri Jan 21, 2011 12:18 am
Profile
Member

Joined: Mon Apr 12, 2010 10:17 pm
Posts: 114
Reply with quote
No problem, the comments were easy to make, I was happy with just the move up/down addition, now you just made it great.
The switch stuff works exactly like VS too, just to my liking, if only switch was a real jumptable in AS3 that would be even better & I'd prob get more use from it :(

Ok, I see what your saying about the SmartPaste(),
I was too busy thinking about the end of the selection when I should of been thinking about the start...
Was trying to select from start of indent on line, instead of col 1, with the keyboard... a quick tap on the Home key fixes all that. And it's not so hard to just make a margin select instead of free select with mouse anyway, I think I'm just so used to adopting all sorts of tactics to make a selection with mouse to circumvent the old paste behaviour, but you just fixed all those probs anyway :D

I just wish there was a way to stop that thing in FD where it inserts extra blank line when you paste a selection made that includes end of last line. But, thats just so minor, who cares...
edit:Oh I just realised this complaint about extra blank line doesn't make any sense if pasting to col 1 anyway, it's perfect behaviour to shift the existing line down...
please ignore...

Thanx again.


Fri Jan 21, 2011 1:33 am
Profile
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
jjase wrote:
Was trying to select from start of indent on line, instead of col 1, with the keyboard...

Okay, I understand what you wanted now.

As you mention, the start of the selection is important, since that's where I determine the indentation of the pasted block. And while it might be possible to detect the case you describe, it wouldn't always work if the line you cursor down to was just an EOL, and thus not picking up the 'wrap-around' of the first line space. And it wouldn't ever work right for cursor positions in the middle of a line.

OTOH, It occurs to me that the action of holding the shift key, and moving the cursor straight down would be more useful if that just selected whole lines for code-type documents. It would make it a bit easier to select lines with the keyboard, and one could always get character-based selections with either the mouse, or by moving the cursor left/right any time during the selection.

Not sure if that's easily implemented, but I'll take a look.

--
John


Fri Jan 21, 2011 6:31 am
Profile
Admin

Joined: Tue Aug 30, 2005 6:14 pm
Posts: 3043
Location: Finland
Reply with quote
John: I often try to make the code shorter to read with one liners. And using this is just a personal preference to seperate member methods from static ones.

One thing came to my mind about automatic intendation. Should it be a language preference instead of a global one? This way FD would support different languages too.


Fri Jan 21, 2011 7:31 am
Profile WWW
Admin

Joined: Wed Aug 31, 2005 7:27 am
Posts: 12172
Location: London
Reply with quote
I suggest this auto-indentation feature is under a setting, and maybe disabled by default until it is properly tested by many users.


Fri Jan 21, 2011 9:37 am
Profile WWW
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
Mika: First impression, is that a user would either like the feature, or not, and that choice wouldn't vary per language. Meaning that forcing them to enable/disable it across multiple language settings would be more of a hassle. The "Indenting" section seems like a natural place to find it.

You could in theory cover both bases with individual language override settings that default to "use global", but that's probably overkill unless and until language-specific issues turn up. It also means that using a global setting now can still be beneficial, even if you decide to implement a per-language preference later on.

A language preference for "Indent after case:" is still something that should be added, to avoid annoying users that expect it the other way.

Philippe: If it's enabled right now, we're likely to get much more testing of the feature (which I agree it needs), rather than relying on users to find out that it exists, and decide to turn it on. If the feature has problems or annoys people, then we fix it and/or disable it by default before it goes to a more public release.


Fri Jan 21, 2011 12:07 pm
Profile
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
I took a look at the concept of selecting full lines with the keyboard, and had a few ideas.

Based on the principles that multi-line selections with arbitrary character positions within the lines are the least useful variety (in code-type documents), and that rectangular selections can't be made from the keyboard at all (unless I missed something), I think the following procedure would offer much easier keyboard selections.

To select full lines, use Shift-Up/Down from anywhere on the line. (Basically FD just moves the selection start and end positions to the start of the lines that
the start/end marks are on).

To select by characters, the *first* keypress must be Shift Left or Right to begin the selection. Then you can use Shift Up/Down to extend the selection in character mode.

To select a rectangular block, first use Shift Up/Down to select the height (full lines will be selected so far), and then Shift Left/Right to select the rectangular block. As in, Shift Left/Right switches to a rectangular selection if it's currently in line-mode. (And further presses of Shift Left/Right/Up/Down extend that rectangular selection).


At first glance, it looked like this would be easy to implement in MethodCallTip.cs, in HandleKeys(). The detection of the shifted arrow keys is right there, calling through to functions like sci.LineDownExtend(). Except that this code doesn't seem to be executed when editing files. In fact, a breakpoint on the ScintillaControl::LineDownExtend() is never hit when doing Shift-Down. So I'm at a loss for where this functionality is accessed.

Any help?


Sat Jan 22, 2011 12:03 am
Profile
Member

Joined: Mon Apr 12, 2010 10:17 pm
Posts: 114
Reply with quote
I think your fairly spot-on there with the shift up|down meaning your going to be wanting to adjust whole lines.
At least that's what it will mean to me in almost all cases I'm doing a shift up|down selection.
I really can't remember ever making partial selections left|right after wanting to go up|down.

That would be nice if you could achieve that, sort of like the move up|down does it now, it selects the whole lines automatically from an arbitrary cursor position before moving them.
I'm actually quite happy with the way it works now. All I needed was the the little col1 tip & it all works well.
Maybe if you leave it as is, you will have to make some note somewhere like on paste tooltip or against one of the settings or something to help dummies like me to figure to goto col1.
Your only ever as far as Alt|Home away from col1 anyway. So goto col1,make selection up|down & cut/paste away.... easy...
But hey... if you can make it easier/better then I wont be complaining. :wink:

Little bug:
ReindentLines() method.
String lineComment = Configuration.GetLanguage(ConfigurationLanguage).linecomment;
is returning null for xml, so obviously giving a bit of grief to StartsWith() etc...
I notice another thread you asked how to get this info other than that other method that defaults to String.Empty so maybe you just haven't tested this for xml recently.
Maybe you want String.IsNullOrEmpty(lineComment) instead of != ""
but anyway you probably don't need my advice.

I'm sure Mika or Philippe will have some much better answers, but if it helps you in the meantime, I think you'll find the callTip will have to be active to get any use of it's HandleKeys() methods.
You might have to go back to UITools to pick up some fulltime keyEvents in it's HandleKeys() handler or further back still.


Sat Jan 22, 2011 3:27 pm
Profile
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
jjase wrote:
That would be nice if you could achieve that, sort of like the move up|down does it now, it selects the whole lines automatically from an arbitrary cursor position before moving them.
The move up|down has an advantage, in that the function *only* works on full lines, so there's never any ambiguity. The selection on partial lines should still be possible, because it's a valid (albeit rare) operation. But it would still be better (easier, and more consistent with the move up/down) if the full line selection could work with just the Shift-Up/Down.

Quote:
String lineComment = Configuration.GetLanguage(ConfigurationLanguage).linecomment;
is returning null for xml...
You're right, I forgot to test XML after making that change. There are three places where this needs to be fixed, and a patch file is enclosed at the bottom.

Quote:
I think you'll find the callTip will have to be active to get any use of it's HandleKeys() methods.
You're right, that's when this call gets used. I'll admit the file name should have given me a clue, but it was the only reference in the project to "LineDownExtend", so that's why I tried it. I'll take a peek at the references you mentioned. Thanks.

Here's the patch for the bug fix:
Code:
Index: ScintillaControl.cs
===================================================================
--- ScintillaControl.cs   (revision 1714)
+++ ScintillaControl.cs   (working copy)
@@ -6084,7 +6084,7 @@
                 pasteStr = GetLine(line).Trim();
                 if (pasteStr != "")
                 {
-                    if (lineComment != "" && pasteStr.StartsWith(lineComment))
+                    if (!String.IsNullOrEmpty(lineComment) && pasteStr.StartsWith(lineComment))
                     {
                         // Indent of the first commented line
                         if (commentIndent < 0) commentIndent = GetLineIndentation(line);
@@ -6114,7 +6114,7 @@
                     }
                     else
                     {
-                        if (lineComment == "" || !destStr.StartsWith(lineComment))
+                        if (String.IsNullOrEmpty(lineComment) || !destStr.StartsWith(lineComment))
                         {
                             destIndent = GetLineIndentation(line); // destination indent at first code-line
                             if (IsControlBlock(destStr) < 0)
@@ -6190,7 +6190,7 @@
             if (startIndex >= 0)
             {
                 String lineComment = Configuration.GetLanguage(ConfigurationLanguage).linecomment;
-                if (lineComment != "")
+                if (!String.IsNullOrEmpty(lineComment))
                 {
                     int slashIndex = str.LastIndexOf(lineComment);
                     if (slashIndex >= startIndex) str = str.Substring(0, slashIndex);


Sat Jan 22, 2011 4:16 pm
Profile
Member

Joined: Thu Feb 09, 2006 10:58 am
Posts: 1095
Location: Israel
Reply with quote
By the way: ALT+SHIFT+CURSORS make rectangular selection.

_________________
MovieClipCommander


Sat Jan 22, 2011 7:09 pm
Profile
Member

Joined: Wed Sep 08, 2010 3:51 pm
Posts: 63
Reply with quote
Thanks IAP, I thought sure I had tried that combo, since it's the obvious choice. But it does work.

That means the proposed Shift-Cursor behavior could be simplified to handle only the character-based and line-based options, and Shift Left/Right would switch to a character selection at any time.

jjase, I tried out UITools, but it's HandleKey method isn't called during normal editing. None of the other instances of "HandleKey" appear to provide the hook I was after. So I'm still not sure where the Shift-cursor behavior can be altered.


Sun Jan 23, 2011 5:34 am
Profile
Admin

Joined: Tue Aug 30, 2005 6:14 pm
Posts: 3043
Location: Finland
Reply with quote
Applied to SVN.


Sun Jan 23, 2011 10:21 am
Profile WWW
Display posts from previous:  Sort by  
Reply to topic   [ 23 posts ]  Go to page 1, 2  Next

Who is online

Users browsing this forum: No registered users and 1 guest


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group.
Designed by ST Software for PTF.