jagregory / boolangstudio forked from olsonjeffery/boolangstudio

Boo language integration for Visual Studio 2008

boolangstudio / Source / BooLangService / LineIndenter.cs
100644 115 lines (98 sloc) 4.415 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TextManager.Interop;
 
namespace BooLangService
{
    /// <summary>
    /// Checks the current users line and indents or dedents the next one.
    /// </summary>
    public class LineIndenter
    {
        // matches comments on the end of a line
        private readonly Regex removeCommentsRegExp = new Regex(@".*((?:#|\/\/|\/\*).*)$", RegexOptions.Compiled);
        // matches a dedenting code line (pass, return, return xxx, return if x = x, return xxx unless x = x, etc...)
        private readonly Regex dedentRegExp = new Regex(@"(?:return|pass)(?:[\t ]?[\w]*[\t ]?(?<exp>(?:if|unless)))?", RegexOptions.Compiled);
        private const string Indent = " ";
        private readonly BooSource source;
 
        public LineIndenter(BooSource source)
        {
            this.source = source;
        }
 
        /// <summary>
        /// Changes the indentation of the current line based on the last
        /// bit of code typed.
        /// </summary>
        /// <param name="changedArea">Area of changed code</param>
        public void ChangeIndentation(TextSpan changedArea)
        {
            TextSpan area = changedArea;
            string text = source.GetText(area);
            string line = source.GetTextUptoPosition(area.iStartLine, area.iStartIndex);
 
            if (!line.EndsWith(text))
            {
                // if commit includes last bit of text typed, the line won't have it yet
                // so stick it on for our parsing sanity
                // ** probably not the best thing to be doing here, but we can't just
                // get the whole line, because we may be pressing return in the centre
                // of it
                line += text;
            }
 
            if (RequiresIndent(line))
            {
                source.SetText(area, text + Indent); // add an indent to the end of the changed text
                MoveCaret(Indent.Length); // move the caret to the new end of line
            }
            else if (RequiresDedent(line))
            {
                source.SetText(area, text.Remove(text.Length - Indent.Length)); // remove an indent
                MoveCaret(-Indent.Length); // move caret in a bit
            }
        }
 
        /// <summary>
        /// Moves the caret a set number of columns.
        /// </summary>
        /// <remarks>
        /// Use negative numbers to move left.
        /// </remarks>
        /// <param name="amount">Number of columns to move.</param>
        private void MoveCaret(int amount)
        {
            IVsTextView view = source.LanguageService.GetPrimaryViewForSource(source);
 
            int caretLine = 0;
            int caretCol = 0;
 
            view.GetCaretPos(out caretLine, out caretCol);
            view.SetCaretPos(caretLine, caretCol + amount);
        }
 
        /// <summary>
        /// Determines whether the current line should have the newline indented.
        /// </summary>
        /// <param name="line">Current line</param>
        /// <returns>Whether newline line should be indented</returns>
        private bool RequiresIndent(string line)
        {
            line = PrepareLine(line); // cheat and make parsing easier
 
            return line.EndsWith(":");
        }
 
        private bool RequiresDedent(string line)
        {
            line = PrepareLine(line); // cheat and make parsing easier
            Match dedentMatch = dedentRegExp.Match(line);
 
            // my regex foo is not strong enough to do this in one, so match
            // a dedent line (i.e. return) but then check if it ends with an
            // expression (i.e. return "value" if x = 10). Only indent if
            // there isn't an expression
            if (dedentMatch.Success && !dedentMatch.Groups["exp"].Success)
                return true;
 
            return false;
        }
 
        private string PrepareLine(string line)
        {
            // remove comments, makes parsing easier
            Match commentMatch = removeCommentsRegExp.Match(line);
 
            if (commentMatch != null && commentMatch.Groups.Count >= 2)
                line = line.Remove(line.IndexOf(commentMatch.Groups[1].Value));
 
            // get rid of trailing whitespace
            line = line.TrimEnd();
 
            return line;
        }
    }
}