-
Notifications
You must be signed in to change notification settings - Fork 5.3k
/
WhiteSpace.java
755 lines (672 loc) · 27.9 KB
/
WhiteSpace.java
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting;
import com.intellij.formatting.engine.BlockRangesMap;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.util.BitUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
/**
* Object-level representation of continuous white space at document. Either line feed or tabulation or space is considered to be {@code 'white-space'}.
* I.e. document text fragment like {@code '\t \t\t\n\t \t'} may be considered as a continuous white space and may be represented as a {@link WhiteSpace} object.
* <p/>
* Provides number of properties that describe encapsulated continuous white space:
* <ul>
* <li>{@link #getLineFeeds() lineFeeds};</li>
* <li>{@link #getSpaces() spaces};</li>
* <li>{@link #getIndentSpaces()};</li>
* </ul>
* <p/>
* Provides ability to build string representation of the managed settings taking into consideration user settings for tabulation vs white space usage, tabulation
* size etc.
* <p/>
* Not thread-safe.
*/
public class WhiteSpace {
private static final char LINE_FEED = '\n';
private final int myStart;
private int myEnd;
private int mySpaces;
private int myIndentSpaces;
private int myInitialLastLinesSpaces;
private int myInitialLastLinesTabs;
private CharSequence myInitial;
private int myFlags;
private boolean myForceSkipTabulationsUsage;
private boolean myIsBeforeCodeBlockEnd;
private static final byte FIRST_MASK = 1;
private static final byte SAFE_MASK = 0x2;
private static final byte KEEP_FIRST_COLUMN_MASK = 0x4;
private static final byte LINE_FEEDS_ARE_READ_ONLY_MASK = 0x8;
private static final byte READ_ONLY_MASK = 0x10;
private static final byte CONTAINS_LF_INITIALLY_MASK = 0x20;
private static final byte CONTAINS_SPACES_INITIALLY_MASK = 0x40;
private static final int LF_COUNT_SHIFT = 7;
private static final int MAX_LF_COUNT = 1 << 24;
private static final RangesAssert myRangesAssert = new RangesAssert();
/**
* Creates new {@code WhiteSpace} object with the given start offset and a flag that shows if current white space is
* the first white space.
* <p/>
* <b>Note:</b> {@link #getEndOffset() end offset} value is the same as the {@link #getStartOffset() start offset} for
* the newly constructed object. {@link #changeEndOffset(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions)} should be
* called in order to apply desired end offset.
*
* @param startOffset start offset to use
* @param isFirst flag that shows if current white space is the first
*/
public WhiteSpace(int startOffset, boolean isFirst) {
myStart = startOffset;
myEnd = startOffset;
setIsFirstWhiteSpace(isFirst);
}
/**
* Applies new end offset to the current object.
* <p/>
* Namely, performs the following:
* <ol>
* <li>Checks if new end offset can be applied, return in case of negative answer;</li>
* <li>
* Processes all new symbols introduced by the new end offset value, calculates number of line feeds,
* white spaces and tabulations between them and updates {@link #getLineFeeds() lineFeeds}, {@link #getSpaces() spaces},
* {@link #getIndentSpaces() indentSpaces} and {@link #getTotalSpaces() totalSpaces} properties accordingly;
* </li>
* </ol>
*
* @param newEndOffset new end offset value
* @param model formatting model that is used to access to the underlying document text
* @param options indent formatting options
*/
public void changeEndOffset(int newEndOffset, FormattingDocumentModel model, CommonCodeStyleSettings.IndentOptions options) {
final int oldEndOffset = myEnd;
if (newEndOffset == oldEndOffset) return;
if (myStart >= newEndOffset) {
myRangesAssert.assertInvalidRanges(myStart, newEndOffset, model, "some block intersects with whitespace");
}
myEnd = newEndOffset;
TextRange range = new TextRange(myStart, myEnd);
CharSequence oldText = myInitial;
myInitial = model.getText(range);
if (!coveredByBlock(model)) {
myRangesAssert.assertInvalidRanges(myStart, myEnd, model, "nonempty text is not covered by block");
}
if (newEndOffset > oldEndOffset) {
refreshStateOnEndOffsetIncrease(newEndOffset, oldEndOffset, options.TAB_SIZE);
} else {
refreshStateOnEndOffsetDecrease(oldText, newEndOffset, oldEndOffset, options.TAB_SIZE);
}
IndentInside indent = IndentInside.getLastLineIndent(myInitial);
myInitialLastLinesSpaces = indent.whiteSpaces;
myInitialLastLinesTabs = indent.tabs;
setFlag(CONTAINS_LF_INITIALLY_MASK, getLineFeeds() > 0);
final int totalSpaces = getTotalSpaces();
setFlag(CONTAINS_SPACES_INITIALLY_MASK, totalSpaces > 0);
}
/**
* Allows to check if {@code 'myInitial'} property value stands for continuous white space text.
* <p/>
* The text is considered to be continuous {@code 'white space'} at following cases:
* <ul>
* <li>{@code 'myInitial'} is empty string or string that contains white spaces only;</li>
* <li>{@code 'myInitial'} is a {@code CDATA} string which content is empty or consists from white spaces only;</li>
* <li>{@code 'myInitial'} string belongs to the same {@link PsiWhiteSpace} element;</li>
* </ul>
*
* @param model formatting model that is used to provide access to the {@code PSI API} if necessary
* @return {@code true} if {@code 'myInitial'} property value stands for white space;
* {@code false} otherwise
*/
private boolean coveredByBlock(final FormattingDocumentModel model) {
if (myInitial == null) return true;
if (model.containsWhiteSpaceSymbolsOnly(myStart, myEnd)) return true;
if (!(model instanceof FormattingDocumentModelImpl)) return false;
PsiFile psiFile = ((FormattingDocumentModelImpl)model).getFile();
PsiElement start = psiFile.findElementAt(myStart);
PsiElement end = psiFile.findElementAt(myEnd-1);
return start == end && start instanceof PsiWhiteSpace; // there maybe non-white text inside CDATA-encoded injected elements
}
private void refreshStateOnEndOffsetIncrease(int newEndOffset, int oldEndOffset, int tabSize) {
assert newEndOffset > oldEndOffset;
WhiteSpaceInfo info = parse(myInitial, oldEndOffset - myStart, newEndOffset - myStart, mySpaces + myIndentSpaces, tabSize);
if (info.lineFeeds > 0) {
setLineFeeds(getLineFeeds() + info.lineFeeds);
mySpaces = 0;
myIndentSpaces = 0;
}
mySpaces += info.spaces;
myIndentSpaces += info.indentSpaces;
}
private void refreshStateOnEndOffsetDecrease(CharSequence oldText, int newEndOffset, int oldEndOffset, int tabSize) {
assert newEndOffset < oldEndOffset;
int lineFeedsNumberAtRemovedText = 0;
int spacesNumberAtRemovedText = 0;
int indentSpacesNumberAtRemovedText = 0;
int column = mySpaces + myIndentSpaces;
for (int i = oldEndOffset - 1; i >= newEndOffset; i--) {
switch (oldText.charAt(i)) {
case LINE_FEED: ++lineFeedsNumberAtRemovedText; column = 0; break;
case ' ': ++spacesNumberAtRemovedText; column--; break;
case '\t':
int change = column % tabSize;
if (change == 0) {
change = tabSize;
}
indentSpacesNumberAtRemovedText += change; column -= change; break;
}
}
// There are no line feeds at remained text, hence, we can just subtract number of spaces and indent spaces
// from removed text and finish.
if (getLineFeeds() - lineFeedsNumberAtRemovedText <= 0) {
setLineFeeds(0);
mySpaces -= spacesNumberAtRemovedText;
myIndentSpaces -= indentSpacesNumberAtRemovedText;
return;
}
// There are white spaces at remained text, hence, we need to calculate number of spaces and indent spaces between
// last line feed and new end offset.
int newLineFeedsNumber = getLineFeeds() - lineFeedsNumberAtRemovedText;
assert newLineFeedsNumber >= 0;
newLineFeedsNumber = newLineFeedsNumber < 0 ? 0 : newLineFeedsNumber; // Never expect the defense to be triggered.
setLineFeeds(newLineFeedsNumber);
int startOffset = CharArrayUtil.shiftForwardUntil(oldText, newEndOffset - 1, "\n") + 1;
WhiteSpaceInfo info = parse(oldText, startOffset, newEndOffset, 0, tabSize);
mySpaces = info.spaces;
myIndentSpaces = info.indentSpaces;
}
/**
* Parses information about white space symbols at the target region of the given text.
*
* @param text target text
* @param startOffset target text range's start offset (inclusive)
* @param endOffset target text range's end offset (exclusive)
* @param startColumn given start offset's column. It affects how tab width is calculated, say, a tab symbol which
* occupies four columns, will occupy only three if located at the first column
* @param tabSize tab width in columns
* @return information about white space symbols at the target region of the given text
*/
@NotNull
private static WhiteSpaceInfo parse(@NotNull CharSequence text, int startOffset, int endOffset, int startColumn, int tabSize) {
assert startOffset <= endOffset;
int spaces = 0;
int indentSpaces = 0;
int lineFeeds = 0;
int column = startColumn;
for (int i = startOffset; i < endOffset; i++) {
switch (text.charAt(i)) {
case LINE_FEED:
lineFeeds++;
spaces = 0;
indentSpaces = 0;
column = 0;
break;
case '\t':
int change = tabSize - (column % tabSize);
indentSpaces += change;
column += change;
break;
default: spaces++; column++;
}
}
return new WhiteSpaceInfo(lineFeeds, indentSpaces, spaces);
}
/**
* Builds string that contains line feeds, white spaces and tabulation symbols known to the current {@link WhiteSpace} object.
*
* @param options indentation formatting options
* @return string that contains line feeds, white spaces and tabulation symbols known to the current
* {@link WhiteSpace} object
*/
public String generateWhiteSpace(CommonCodeStyleSettings.IndentOptions options) {
return
new IndentInfo(getLineFeeds(), myIndentSpaces, mySpaces, myForceSkipTabulationsUsage)
.setIndentEmptyLines(myIsBeforeCodeBlockEnd)
.generateNewWhiteSpace(options);
}
/**
* Tries to apply given values to {@link #getSpaces() spaces} and {@link #getIndentSpaces() indentSpaces} properties accordingly.
* <p/>
* The action is not guaranteed to be executed (i.e. the it's not guaranteed that target properties return given values after
* this method call - see {@link #performModification(Runnable)} for more details).
* <p/>
* Moreover, the action is guaranteed to be <b>not</b> executed if {@link #isKeepFirstColumn() keepFirstColumn} property
* is unset and target document string doesn't contain spaces.
*
* @param spaces new value for the {@link #getSpaces() spaces} property
* @param indent new value for the {@link #getIndentSpaces()} indentSpaces} property
*/
public void setSpaces(final int spaces, final int indent) {
if (spaces < 0 || indent < 0) {
throw new IllegalStateException("Neither spaces: " + spaces + " or indent " + indent + " should be null");
}
performModification(() -> {
if (!isKeepFirstColumn() || getFlag(CONTAINS_SPACES_INITIALLY_MASK)) {
mySpaces = spaces;
myIndentSpaces = indent;
}
});
}
private boolean doesNotContainAnySpaces() {
return getTotalSpaces() == 0 && getLineFeeds() == 0;
}
public int getStartOffset() {
return myStart;
}
public int getEndOffset() {
return myEnd;
}
/**
* Execute given action in a safe manner.
* <p/>
* {@code 'Safe manner'} here means the following:
* <ul>
* <li>don't execute the action if {@link #isIsReadOnly() isReadOnly} property value is set to {@code true};</li>
* <li>
* ensure that number of line feeds after action execution is preserved if line feeds are
* {@link #isLineFeedsAreReadOnly() read only};
* </li>
* <li>
* ensure the following if {@link #isIsSafe() isSafe} property is set to {@code true}:
* <ul>
* <li>
* cut all white spaces and line feeds appeared after given action execution to single white space if there
* were no line feeds and white spaces before;
* </li>
* </ul>
* </li>
* </ul>
*
* @param action action that should be performed in a safe manner
*/
private void performModification(Runnable action) {
if (isIsReadOnly()) return;
final boolean before = doesNotContainAnySpaces();
final int lineFeedsBefore = getLineFeeds();
action.run();
if (isLineFeedsAreReadOnly()) {
setLineFeeds(lineFeedsBefore);
}
if (isIsSafe()) {
final boolean after = doesNotContainAnySpaces();
if (before && !after) {
// Actions below seem to be useless if 'after' value is 'false'. Are kept as historical heritage.
mySpaces = 0;
myIndentSpaces = 0;
setLineFeeds(0);
}
else if (!before && after) {
mySpaces = 1;
myIndentSpaces = 0;
}
}
}
/**
* Tries to arrange {@link #getSpaces() spaces} property value to belong to
* [{@link SpacingImpl#getMinSpaces() min}; {@link SpacingImpl#getMaxSpaces()}] bounds defined by the given spacing object
* if {@link #getTotalSpaces() totalSpaces} property value lays out of the same bounds.
* <p/>
* The action is <b>not</b> performed if there are line feeds configured for the
* current {@link WhiteSpace} object ({@link #getSpaces()} != 0).
*
* @param spaceProperty spacing settings holder
*/
public void arrangeSpaces(final SpacingImpl spaceProperty) {
performModification(() -> {
if (spaceProperty != null) {
if (getLineFeeds() == 0) {
if (spaceProperty.getMinSpaces() >= 0 && getTotalSpaces() < spaceProperty.getMinSpaces()) {
setSpaces(spaceProperty.getMinSpaces(), 0);
}
if (spaceProperty.getMaxSpaces() >= 0 && getTotalSpaces() > spaceProperty.getMaxSpaces()) {
setSpaces(spaceProperty.getMaxSpaces(), 0);
}
}
}
});
}
/**
* Tries to ensure that number of line feeds managed by the current {@link WhiteSpace} is consistent to the settings
* defined at the given spacing property.
*/
public void arrangeLineFeeds(final SpacingImpl spaceProperty, final BlockRangesMap helper) {
performModification(() -> {
if (spaceProperty != null) {
spaceProperty.refresh(helper);
if (spaceProperty.getMinLineFeeds() >= 0 && getLineFeeds() < spaceProperty.getMinLineFeeds()) {
setLineFeeds(spaceProperty.getMinLineFeeds());
}
if (getLineFeeds() > 0) {
if (spaceProperty.getKeepBlankLines() > 0) {
if (getLineFeeds() >= spaceProperty.getKeepBlankLines() + 1) {
setLineFeeds(spaceProperty.getKeepBlankLines() + 1);
}
}
else {
if (getLineFeeds() > spaceProperty.getMinLineFeeds()) {
if (spaceProperty.shouldKeepLineFeeds()) {
setLineFeeds(Math.max(spaceProperty.getMinLineFeeds(), 1));
}
else {
setLineFeeds(spaceProperty.getMinLineFeeds());
if (getLineFeeds() == 0) mySpaces = 0;
}
}
}
if (getLineFeeds() == 1 && !spaceProperty.shouldKeepLineFeeds() && spaceProperty.getMinLineFeeds() == 0) {
setLineFeeds(0);
mySpaces = 0;
}
if (getLineFeeds() > 0 && getLineFeeds() < spaceProperty.getPrefLineFeeds()) {
setLineFeeds(spaceProperty.getPrefLineFeeds());
}
}
} else if (isFirst()) {
setLineFeeds(0);
mySpaces = 0;
}
});
}
/**
* There is a possible case that particular indent info is applied to the code block that is not the first block on a line.
* E.g. we may want to align field name during {@code 'align fields in columns'} processing:
* <pre>
* public class Test {
* private Object o;
* private int {@code <white space to align>}i;
* }
* </pre>
* We may not want to use tabulation characters then even if user configured
* {@link CommonCodeStyleSettings.IndentOptions#USE_TAB_CHARACTER their usage} because we can't be sure how many visual columns
* will be used for tab representation if there are non-white space symbols before it (IJ editor may use different number of columns
* for single tabulation symbol representation).
* <p/>
* Hence, we can ask current white space object to avoid using tabulation symbols.
*
* @param skip indicates if tabulations symbols usage should be suppressed
*/
public void setForceSkipTabulationsUsage(boolean skip) {
myForceSkipTabulationsUsage = skip;
}
/**
* Allows to get information if target text document continuous 'white space' represented by the current object contained line feed
* symbol(s) initially or contains line feed(s) now.
*
* @return {@code true} if this object contained line feeds initially or contains them now; {@code false} otherwise
* @see #containsLineFeedsInitially()
*/
public boolean containsLineFeeds() {
return isIsFirstWhiteSpace() || getLineFeeds() > 0;
}
public int getTotalSpaces() {
return mySpaces + myIndentSpaces;
}
/**
* Tries to ensure that current {@link WhiteSpace} object contains at least one line feed.
*/
public void ensureLineFeed() {
performModification(() -> {
if (!containsLineFeeds()) {
setLineFeeds(1);
mySpaces = 0;
}
});
}
public boolean isReadOnly() {
return isIsReadOnly() || (isIsSafe() && doesNotContainAnySpaces());
}
/**
* @param ws char sequence to check
* @return {@code true} if given char sequence is equal to the target document text identified by
* start/end offsets managed by the current {@link WhiteSpace} object; {@code false} otherwise
*/
public boolean equalsToString(CharSequence ws) {
if (myInitial == null) return ws.length() == 0;
return Comparing.equal(ws,myInitial,true);
}
public void setIsSafe(final boolean value) {
setFlag(SAFE_MASK, value);
}
private void setFlag(final int mask, final boolean value) {
myFlags = BitUtil.set(myFlags, mask, value);
}
private boolean getFlag(final int mask) {
return BitUtil.isSet(myFlags, mask);
}
private boolean isFirst() {
return isIsFirstWhiteSpace();
}
/**
* Allows to get information if current object contained line feed(s) initially. It's considered to contain them if
* line feeds were found at the target text document fragment after
* new {@link #changeEndOffset(int, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions)} end offset appliance}.
*
* @return {@code true} if current object contained line feeds initially; {@code false} otherwise
*/
public boolean containsLineFeedsInitially() {
if (myInitial == null) return false;
return getFlag(CONTAINS_LF_INITIALLY_MASK);
}
/**
* Tries to ensure that number of line feeds and white spaces managed by the given {@link WhiteSpace} object is the
* same as the one defined by the given {@code 'spacing'} object.
* <p/>
* This method may be considered a shortcut for calling {@link #arrangeLineFeeds(SpacingImpl, BlockRangesMap)} and
* {@link #arrangeSpaces(SpacingImpl)}.
*/
public void removeLineFeeds(final SpacingImpl spacing, final BlockRangesMap helper) {
performModification(() -> {
setLineFeeds(0);
mySpaces = 0;
myIndentSpaces = 0;
});
arrangeLineFeeds(spacing, helper);
arrangeSpaces(spacing);
}
public int getIndentOffset() {
return myIndentSpaces;
}
/**
* Allows to get information about number of 'pure' white space symbols at the last line of continuous white space document
* text fragment represented by the current {@link WhiteSpace} object.
* <p/>
* <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target continuous
* white space document fragment:
* <pre>
* ' ws<sub>11</sub>ws<sub>12</sub>
* 'ws<sub>21</sub>'
* </pre>
* <p/>
* Here <code>'ws<sub>nm</sub>'</code> is a m-th white space symbol at the n-th line. {@code 'Spaces'} property of
* {@link WhiteSpace} object for such white-space text has a value not <b>2</b> or <b>3</b> but <b>1</b>.
*
* @return number of white spaces at the last line of target continuous white space text document fragment
*/
public int getSpaces() {
return mySpaces;
}
public void setKeepFirstColumn(final boolean b) {
setFlag(KEEP_FIRST_COLUMN_MASK, b);
}
public void setLineFeedsAreReadOnly() {
setLineFeedsAreReadOnly(true);
}
public void setReadOnly(final boolean isReadOnly) {
setIsReadOnly(isReadOnly);
}
public boolean isIsFirstWhiteSpace() {
return getFlag(FIRST_MASK);
}
public boolean isIsSafe() {
return getFlag(SAFE_MASK);
}
public boolean isKeepFirstColumn() {
return getFlag(KEEP_FIRST_COLUMN_MASK);
}
public boolean isLineFeedsAreReadOnly() {
return getFlag(LINE_FEEDS_ARE_READ_ONLY_MASK);
}
public void setLineFeedsAreReadOnly(final boolean lineFeedsAreReadOnly) {
setFlag(LINE_FEEDS_ARE_READ_ONLY_MASK, lineFeedsAreReadOnly);
}
public boolean isIsReadOnly() {
return getFlag(READ_ONLY_MASK);
}
public void setIsReadOnly(final boolean isReadOnly) {
setFlag(READ_ONLY_MASK, isReadOnly);
}
public void setIsFirstWhiteSpace(final boolean isFirstWhiteSpace) {
setFlag(FIRST_MASK, isFirstWhiteSpace);
}
public StringBuilder generateWhiteSpace(final CommonCodeStyleSettings.IndentOptions indentOptions,
final int offset,
final IndentInfo indent) {
final StringBuilder result = new StringBuilder();
int currentOffset = getStartOffset();
CharSequence[] lines = getInitialLines();
int currentLine = 0;
for (int i = 0; i < lines.length - 1 && currentOffset + lines[i].length() <= offset; i++) {
result.append(lines[i]);
currentOffset += lines[i].length();
result.append(LINE_FEED);
currentOffset++;
currentLine++;
if (currentOffset == offset) {
break;
}
}
final String newIndentSpaces = indent.generateNewWhiteSpace(indentOptions);
result.append(newIndentSpaces);
appendNonWhitespaces(result, lines, currentLine);
if (currentLine + 1 < lines.length) {
result.append(LINE_FEED);
for (int i = currentLine + 1; i < lines.length - 1; i++) {
result.append(lines[i]);
result.append(LINE_FEED);
}
appendNonWhitespaces(result, lines, lines.length-1);
result.append(lines[lines.length - 1]);
}
return result;
}
@NotNull
public IndentInside getInitialLastLineIndent() {
return new IndentInside(myInitialLastLinesSpaces, myInitialLastLinesTabs);
}
private static void appendNonWhitespaces(StringBuilder result, CharSequence[] lines, int currentLine) {
// It looks like regexp usage is too heavy here.
if (currentLine != lines.length && !lines[currentLine].toString().matches("\\s*")) {
result.append(lines[currentLine]);
}
}
/**
* Shows target document text fragment identified by start/end offsets as a sequence of strings.
* <p/>
* <b>Note:</b> arrays usage here is considered to be a historical heritage.
*
* @return target document text fragment as a sequence of strings
*/
private CharSequence[] getInitialLines() {
if (myInitial == null) return new CharSequence[]{""};
final ArrayList<CharSequence> result = new ArrayList<>();
StringBuilder currentLine = new StringBuilder();
for (int i = 0; i < myInitial.length(); i++) {
final char c = myInitial.charAt(i);
if (c == LINE_FEED) {
result.add(currentLine);
currentLine = new StringBuilder();
}
else {
currentLine.append(c);
}
}
result.add(currentLine);
return result.toArray(new CharSequence[0]);
}
/**
* Provides access to the information about indent white spaces at last line of continuous white space text document
* fragment represented by the current {@link WhiteSpace} object.
* <p/>
* {@code 'Indent white space'} here is a white space representation of tabulation symbol. User may define that
* he or she wants to use particular number of white spaces instead of tabulation
* ({@link CommonCodeStyleSettings.IndentOptions#TAB_SIZE}). So, {@link WhiteSpace} object uses corresponding
* number of 'indent white spaces' for each tabulation symbol encountered at target continuous white space text document fragment.
* <p/>
* <b>Note:</b> pay special attention to {@code 'last line'} qualification here. Consider the following target
* continuous white space document fragment:
* <pre>
* ' \t\t
* '\t'
* </pre>
* <p/>
* Let's consider that {@code 'tab size'} is defined as four white spaces (default setting).
* {@code 'IndentSpaces'} property of {@link WhiteSpace} object for such white-space text has a value not
* <b>8</b> or <b>12</b> but <b>4</b>, i.e. tabulation symbols from last line
* only are counted.
*
* @return number of indent spaces at the last line of target continuous white space text document fragment
*/
public int getIndentSpaces() {
return myIndentSpaces;
}
public int getLength() {
return myEnd - myStart;
}
/**
* Provides access to the line feed symbols number at continuous white space text document fragment represented
* by the current {@link WhiteSpace} object.
*
* @return line feed symbols number
*/
public final int getLineFeeds() {
return myFlags >>> LF_COUNT_SHIFT;
}
public void setLineFeeds(final int lineFeeds) {
assert lineFeeds < MAX_LF_COUNT;
final int flags = myFlags;
myFlags &= ~0xFFFFFF80; // keep only seven lower bits, i.e. drop all line feeds registered before if any
myFlags |= (lineFeeds << LF_COUNT_SHIFT);
assert getLineFeeds() == lineFeeds;
assert (flags & 0x7F) == (myFlags & 0x7F);
}
@NotNull
public WhiteSpace setBeforeCodeBlockEnd(boolean isBeforeCodeBlockEnd) {
myIsBeforeCodeBlockEnd = isBeforeCodeBlockEnd;
return this;
}
public TextRange getTextRange() {
return new TextRange(myStart, myEnd);
}
@Override
public String toString() {
return "WhiteSpace(" + myStart + "-" + myEnd + " spaces=" + mySpaces + " LFs=" + getLineFeeds() + ")";
}
private static class WhiteSpaceInfo {
public final int spaces;
public final int indentSpaces;
public final int lineFeeds;
WhiteSpaceInfo(int lineFeeds, int indentSpaces, int spaces) {
this.lineFeeds = lineFeeds;
this.indentSpaces = indentSpaces;
this.spaces = spaces;
}
}
}