tags:

views:

88

answers:

2

this is how i implemented Shift-Tab or decrease indent... the result on screenr

if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift && e.Key == Key.Tab)
{
    // Shift+Tab
    int selStart = txtEditor.SelectionStart;
    int selLength = txtEditor.SelectionLength;
    string selText = txtEditor.SelectedText;
    string text = txtEditor.Text;

    // find new lines that are followed by 1 or more spaces
    Regex regex = new Regex(Environment.NewLine + @"(\s+)");
    Match m = regex.Match(selText);
    string spaces;
    while (m.Success)
    {
        GroupCollection grps = m.Groups;
        spaces = grps[1].Value;
        int i = 0;
        // remove 1 space on each loop to a max of 4 spaces
        while (i < 4 && spaces.Length > 0)
        {
            spaces = spaces.Remove(0, 1);
            i++;
        }
        // update spaces in selText
        selText = selText.Remove(grps[1].Index, grps[1].Length).Insert(grps[1].Index, spaces);

        m = regex.Match(selText, grps[1].Index + spaces.Length);
    }

    // commit changes to selText to text 
    text = text.Remove(selStart, selLength).Insert(selStart, selText);

    // decrease indent of 1st line
    // - find 1st character of selection
    regex = new Regex(@"\w");
    m = regex.Match(text, selStart);
    int start = selStart;
    if (m.Success) {
        start = m.Index;
    }
    // - start search for spaces 
    regex = new Regex(Environment.NewLine + @"(\s+)", RegexOptions.RightToLeft);
    m = regex.Match(text, start);
    if (m.Success) {
        spaces = m.Groups[1].Value;
        int i = 0;
        while (i < 4 && spaces.Length > 0) {
            spaces = spaces.Remove(0, 1); // remove 1 space
            i++;
        }
        text = text.Remove(m.Groups[1].Index, m.Groups[1].Length).Insert(m.Groups[1].Index, spaces);
        selStart = m.Groups[1].Index;
    }

    txtEditor.Text = text;
    txtEditor.SelectionStart = selStart;
    txtEditor.SelectionLength = selText.Length;
    e.Handled = true;
}

the code looks messy and i wonder if theres a better way.

A: 

I'm thinking freely as I have never implemented a text editor.

What if you represent each line by an object with an indentation property, which is reflected in the rendering of the line. Then it would be easy to increase and decrease the indent.

adamse
this editor is more of a personal project. anyways how do u represent each line by an object. do you mean i split the text using new lines as separator and keep these lines in an array?
jiewmeng
Something like that, it would probably be inefficient as hell. Other problems that may arise is multi-partial-line selections. Maybe you want several different representations of the text.
adamse
+2  A: 

Personally, I wouldn't use Regex for this.

Untested, probably needs modification:

public static class StringExtensions
{
 // Removes leading white-spaces in a string up to a maximum
 // of 'level' characters
 public static string ReduceIndent(this string line, int level)
 { 
   // Produces an IEnumerable<char> with the characters 
   // of the string verbatim, other than leading white-spaces
   var unindentedChars = line.SkipWhile((c, index) => char.IsWhiteSpace(c) && index < level);

   return new string(unindentedChars.ToArray());
 }


 // Applies a transformation to each line of a string and returns the
 // transformed string
 public static string LineTransform(this string text, Func<string,string> transform)
 {
   //Splits the string into an array of lines
   var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

   //Applies the transformation to each line
   var transformedLines = lines.Select(transform);

   //Joins the transformed lines into a new string
   return string.Join(Environment.NewLine, transformedLines.ToArray());
 } 
}
 ... 

if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift && e.Key == Key.Tab)
{                             
  // Reduces the indent level of the selected text by applying the
  // 'ReduceIndent' transformation to each line of the text.
  string replacement = txtEditor.SelectedText
                                .LineTransform(line => line.ReduceIndent(4));

  int selStart = txtEditor.SelectionStart;
  int selLength = txtEditor.SelectionLength;

  txtEditor.Text = txtEditor.Text
                            .Remove(selStart, selLength)
                            .Insert(selStart, replacement);

  txtEditor.SelectionStart = selStart;
  txtEditor.SelectionLength = replacement.Length;
  e.Handled = true;
}   

EDIT:

Added comments to the code as per the request of the OP.

For more info:

Ani
jiewmeng
@jiewmeng: Ok, will do; no worries.
Ani
thanks for updates :) need to digest it now.
jiewmeng
theres 1 thing i dont understand about the `SkipWhile` when i type `str.SkipWhile(` the intellisense tells me that it accepts `Func<char, bool>` but in `(c, index) => ...` its taking in a `char` and `int` right, tho it returns a `bool`?
jiewmeng
hi i tried it out and the problem i face is from `var lines = text.Split(Enivironment.NewLine.ToCharArray());` i think its better illustrated in [pastebin]. as u can see, `Split(char[])` will split by `\r` OR `\n` so foreach newline, it will produce the `text + "" + nextLine` is there another way of spliting? or maybe i compensate by ignoring `""` empty strings?
jiewmeng
@jiewmeng: You are on the right track as regards `SkipWhile.` There is another overload that accepts a `Func<TSource, int, bool>` (`TSource` here is `char`, obviously). The link in my answer points to the right overload. Can you post the pastebin link? I can't access it from your comment.
Ani
@jiewmeng: I think I understood your issue. Updated the answer to modify the split to `var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);`Note that the selection must include the leading white-spaces on the first line for it to work. If this is an issue, you must backtrack on the selected-text until you find the start of the first line.
Ani
oh yes the [pastebin link](http://pastebin.com/yTy6mwM0). and yes i did think of the need to include leading white space. i need to think abt how to implement that. probably search for the nearest newline
jiewmeng
@jiewmeng: Yes. Something like this? `int selStartIncludingAllOfFirstSelLine = text.Substring(0, selStart + 1).LastIndexOf(Environment.NewLine);``if (selStartIncludingAllOfFirstSelLine == -1) selStartIncludingAllOfFirstSelLine = 0;`
Ani
thanks, u are a genius! after some testing tho, i found `Split()` having another side effect, illustrated [pastebin](http://pastebin.com/vfjN0cCJ). basically, since it now ignores empty strings, if my selected text has "empty" lines... hmm ... btw what does `new[]` do?
jiewmeng
update: fixed using [`Regex.Split()`](http://msdn.microsoft.com/en-us/library/ze12yx1d.aspx) instead of `string.Split()`
jiewmeng
@jiewmeng: Good to hear. `new[]` creates an array and returns a reference to it. `new[] { Environment.NewLine }` is shorthand for `new string[1] { Environment.NewLine }` - the compiler is smart and understands what I mean.
Ani
oh and 1 thing i forgot to ask why is there a need for +1 in `selStartIncludingAllOfFirstSelLine = text.Substring(0, selStart + 1)`
jiewmeng