Welcome to mirror list, hosted at ThFree Co, Russian Federation.

WhitespaceExtensions.cs « TextDataUtil « Util « Text « Editor « src - github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: b8714da0fb2b31aeeb3073acea7d6a93b8d6e1aa (plain)
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.VisualStudio.Text.Data.Utilities
{
    internal static class WhitespaceExtensions
    {
        public const string CRLF_LITERAL = "\r\n";
        public const string CR_LITERAL = "\r";
        public const string LF_LITERAL = "\n";
        public const string NEL_LITERAL = "\u0085";
        public const string LS_LITERAL = "\u2028";
        public const string PS_LITERAL = "\u2029";

        /// <summary>
        /// Given a line, returns the kind of newline that appears at the end of the line.
        /// </summary>
        /// <param name="line">The line to inspect</param>
        /// <returns>The kind of newline appearing at the end of the line, or null if this is the last line of the document.</returns>
        public static NewlineState.LineEnding? GetLineEnding(this ITextSnapshotLine line)
        {
            if (line.LineBreakLength == 0)
            {
                return null;
            }

            if (line.LineBreakLength == 2)
            {
                return NewlineState.LineEnding.CRLF;
            }

            switch (line.Snapshot[line.End])
            {
                case '\r':
                    return NewlineState.LineEnding.CR;
                case '\n':
                    return NewlineState.LineEnding.LF;
                case '\u0085':
                    return NewlineState.LineEnding.NEL;
                case '\u2028':
                    return NewlineState.LineEnding.LS;
                case '\u2029':
                    return NewlineState.LineEnding.PS;
                default:
                    throw new ArgumentException($"Unexpected newline character {line.Snapshot[line.End]}", nameof(line));
            }
        }

        public static LeadingWhitespaceState.LineLeadingCharacter GetLeadingCharacter(this ITextSnapshotLine line)
        {
            if (line.Length == 0)
            {
                return LeadingWhitespaceState.LineLeadingCharacter.Empty;
            }

            switch (line.Snapshot[line.Start])
            {
                case ' ':
                    return LeadingWhitespaceState.LineLeadingCharacter.Space;
                case '\t':
                    return LeadingWhitespaceState.LineLeadingCharacter.Tab;
                default:
                    return LeadingWhitespaceState.LineLeadingCharacter.Printable;
            }
        }

        /// <summary>
        /// Takes a string representation of a line ending and returns the corresponding line ending.
        /// </summary>
        /// <remarks>This method will involve allocating strings, if at all posibile, use LineEndingFromSnapshotLine.</remarks>
        /// <param name="lineEndingString">A string representation of a line ending.</param>
        /// <returns>The corresponding LineEnding enumeration value. Null if the string isn't a recognized line ending.</returns>
        public static NewlineState.LineEnding? LineEndingFromString(string lineEndingString)
        {
            switch (lineEndingString)
            {
                case CRLF_LITERAL:
                    return NewlineState.LineEnding.CRLF;
                case CR_LITERAL:
                    return NewlineState.LineEnding.CR;
                case LF_LITERAL:
                    return NewlineState.LineEnding.LF;
                case NEL_LITERAL:
                    return NewlineState.LineEnding.NEL;
                case LS_LITERAL:
                    return NewlineState.LineEnding.LS;
                case PS_LITERAL:
                    return NewlineState.LineEnding.PS;
                default:
                    return null;
            }
        }

        public static string StringFromLineEnding(this NewlineState.LineEnding lineEnding)
        {
            switch (lineEnding)
            {
                case NewlineState.LineEnding.CRLF:
                    return CRLF_LITERAL;
                case NewlineState.LineEnding.CR:
                    return CR_LITERAL;
                case NewlineState.LineEnding.LF:
                    return LF_LITERAL;
                case NewlineState.LineEnding.NEL:
                    return NEL_LITERAL;
                case NewlineState.LineEnding.LS:
                    return LS_LITERAL;
                case NewlineState.LineEnding.PS:
                    return PS_LITERAL;
                default:
                    // We shouldn't have any more, just return CRLF as paranoia.
                    return CRLF_LITERAL;
            }
        }

        /// <summary>
        /// Normalizes the given buffer to match the given newline string on every line
        /// </summary>
        /// <returns>True if the buffer was changed. False otherwise.</returns>
        public static bool NormalizeNewlines(this ITextBuffer buffer, string newlineString)
        {
            using (var edit = buffer.CreateEdit())
            {
                foreach (var line in edit.Snapshot.Lines)
                {
                    if (line.LineBreakLength != 0)
                    {
                        // Calling line.GetLineBreakText allocates a string. Since we only have 1 2-character newline to worry about
                        // we can do this without that allocation by comparing characters directly.
                        if (line.LineBreakLength != newlineString.Length || edit.Snapshot[line.End] != newlineString[0])
                        {
                            // Intentionally ignore failed replaces. We'll do the best effort change here.
                            edit.Replace(new Span(line.End, line.LineBreakLength), newlineString);
                        }
                    }
                }

                if (edit.HasEffectiveChanges)
                {
                    return edit.Apply() != edit.Snapshot;
                }
                else
                {
                    // We didn't have to do anything
                    return false;
                }
            }
        }

        /// <summary>
        /// Normalizes the given buffer to match the given whitespace stype.
        /// </summary>
        /// <returns>True if the buffer was changed. False otherwise.</returns>
        public static bool NormalizeLeadingWhitespace(this ITextBuffer buffer, int tabSize, bool useSpaces)
        {
            using (var edit = buffer.CreateEdit())
            {
                var whitespaceCache = new string[100];

                foreach (var line in edit.Snapshot.Lines)
                {
                    if (line.Length > 0)
                    {
                        AnalyzeWhitespace(line, tabSize, out int whitespaceCharacterLength, out int column);
                        if (column > 0)
                        {
                            var whitespace = GetWhitespace(whitespaceCache, tabSize, useSpaces, column);

                            if ((whitespace.Length != whitespaceCharacterLength) || !ComparePrefix(line, whitespace))
                            {
                                edit.Replace(new Span(line.Start, whitespaceCharacterLength), whitespace);
                            }
                        }
                    }
                }

                return edit.Apply() != edit.Snapshot;
            }
        }

        private static void AnalyzeWhitespace(ITextSnapshotLine line, int tabSize, out int whitespaceCharacterLength, out int column)
        {
            column = 0;
            whitespaceCharacterLength = 0;
            while (whitespaceCharacterLength < line.Length)
            {
                var c = (line.Start + whitespaceCharacterLength).GetChar();
                if (c == ' ')
                    ++column;
                else if (c == '\t')
                    column = ((1 + column / tabSize) * tabSize);
                else
                    break;

                ++whitespaceCharacterLength;
            }
        }

        private static string GetWhitespace(string[] whitespaceCache, int tabSize, bool useSpaces, int column)
        {
            string whitespace;
            if ((column < whitespaceCache.Length) && (whitespaceCache[column] != null))
            {
                whitespace = whitespaceCache[column];
            }
            else
            {
                if (useSpaces)
                {
                    whitespace = new string(' ', column);
                }
                else
                {
                    whitespace = new string('\t', column / tabSize);
                    var spaces = column % tabSize;
                    if (spaces != 0)
                        whitespace += new string(' ', spaces);
                }

                if (column < whitespaceCache.Length)
                    whitespaceCache[column] = whitespace;
            }

            return whitespace;
        }

        private static bool ComparePrefix(ITextSnapshotLine line, string whitespace)
        {
            for (int i = 0; (i < whitespace.Length); ++i)
                if ((line.Start + i).GetChar() != whitespace[i])
                    return false;

            return true;
        }

        /// <summary>
        /// Normalizes the given buffer to match the given newline state's line endings if they are consistent.
        /// </summary>
        /// <returns>True if the buffer was changed. False otherwise.</returns>
        public static bool NormalizeNewlines(this NewlineState newlineState, ITextBuffer buffer)
        {
            // Keep this method in sync with the other NormalizeNewlines overload below.
            if (!newlineState.HasConsistentLineEndings || !newlineState.InferredLineEnding.HasValue)
            {
                // Right now we expect people to overwhelmingly start from project templates, item templates, or cloned code,
                // which will give them at least one newline in a given document. If they don't then we're taking the easy
                // route here, to not do anything. That could be improved upon, but we're waiting for user feedback to justify
                // further work in this area.
                return false;
            }

            /* Potential optimization, check to see if text contains any newlines that would be replaced, and if not, just return text and avoid allocations */
            string newlineString = newlineState.InferredLineEnding.Value.StringFromLineEnding();

            return buffer.NormalizeNewlines(newlineString);
        }

        public static string NormalizeNewlines(this NewlineState newlineState, string text)
        {
            // Keep this method in sync with the other NormalizeNewlines overload above
            if (!newlineState.HasConsistentLineEndings || !newlineState.InferredLineEnding.HasValue)
            {
                // Right now we expect people to overwhelmingly start from project templates, item templates, or cloned code,
                // which will give them at least one newline in a given document. If they don't then we're taking the easy
                // route here, to not do anything. That could be improved upon, but we're waiting for user feedback to justify
                // further work in this area.
                return text;
            }

            /* Potential optimization, check to see if text contains any newlines that would be replaced, and if not, just return text and avoid allocations */
            string newlineString = newlineState.InferredLineEnding.Value.StringFromLineEnding();

            // In the perverse case, where we have a string full of "\n\n\n\n\n\n" and the document wants \r\n, we can only ever double the size of the string.
            var strBuilder = new StringBuilder(text.Length * 2);

            for (int i = 0; i < text.Length; i++)
            {
                switch (text[i])
                {
                    case '\r':
                        if (i < (text.Length - 1) && text[i + 1] == '\n')
                        {
                            i++;
                        }
                        goto case '\n';
                    case '\n':
                    case '\u0085':
                    case '\u2028':
                    case '\u2029':
                        strBuilder.Append(newlineString);
                        break;
                    default:
                        strBuilder.Append(text[i]);
                        break;
                }
            }

            return strBuilder.ToString();

        }
    }
}