1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Utility\File;
6: use Stringable;
7:
8: /**
9: * @api
10: */
11: final class Indentation
12: {
13: /** @readonly */
14: public bool $InsertSpaces;
15: /** @readonly */
16: public int $TabSize;
17:
18: public function __construct(bool $insertSpaces, int $tabSize)
19: {
20: $this->InsertSpaces = $insertSpaces;
21: $this->TabSize = $tabSize;
22: }
23:
24: /**
25: * Guess the indentation used in a file or stream
26: *
27: * Derived from VS Code's `indentationGuesser`.
28: *
29: * Returns `$default` if `$resource` appears to use the default indentation.
30: *
31: * @param Stringable|string|resource $resource
32: * @param Stringable|string|null $uri
33: *
34: * @link https://github.com/microsoft/vscode/blob/860d67064a9c1ef8ce0c8de35a78bea01033f76c/src/vs/editor/common/model/indentationGuesser.ts
35: */
36: public static function from(
37: $resource,
38: ?self $default = null,
39: bool $alwaysGuessTabSize = false,
40: $uri = null
41: ): self {
42: $handle = File::maybeOpen($resource, 'r', $close, $uri);
43:
44: $lines = 0;
45: $linesWithTabs = 0;
46: $linesWithSpaces = 0;
47: $diffSpacesCount = [2 => 0, 0, 0, 0, 0, 0, 0];
48:
49: $prevLine = '';
50: $prevOffset = 0;
51: while ($lines < 10000) {
52: $line = @fgets($handle);
53: if ($line === false) {
54: File::checkEof($handle, $uri);
55: break;
56: }
57:
58: $lines++;
59:
60: $line = rtrim($line);
61: if ($line === '') {
62: continue;
63: }
64:
65: $length = strlen($line);
66: $spaces = 0;
67: $tabs = 0;
68: for ($offset = 0; $offset < $length; $offset++) {
69: if ($line[$offset] === "\t") {
70: $tabs++;
71: } elseif ($line[$offset] === ' ') {
72: $spaces++;
73: } else {
74: break;
75: }
76: }
77:
78: if ($tabs) {
79: $linesWithTabs++;
80: } elseif ($spaces > 1) {
81: $linesWithSpaces++;
82: }
83:
84: $minOffset = $prevOffset < $offset ? $prevOffset : $offset;
85: for ($i = 0; $i < $minOffset; $i++) {
86: if ($prevLine[$i] !== $line[$i]) {
87: break;
88: }
89: }
90:
91: $prevLineSpaces = 0;
92: $prevLineTabs = 0;
93: for ($j = $i; $j < $prevOffset; $j++) {
94: if ($prevLine[$j] === ' ') {
95: $prevLineSpaces++;
96: } else {
97: $prevLineTabs++;
98: }
99: }
100:
101: $lineSpaces = 0;
102: $lineTabs = 0;
103: for ($j = $i; $j < $offset; $j++) {
104: if ($line[$j] === ' ') {
105: $lineSpaces++;
106: } else {
107: $lineTabs++;
108: }
109: }
110:
111: $_prevLine = $prevLine;
112: $_prevOffset = $prevOffset;
113: $_line = $line;
114:
115: $prevLine = $line;
116: $prevOffset = $offset;
117:
118: if (
119: ($prevLineSpaces && $prevLineTabs)
120: || ($lineSpaces && $lineTabs)
121: ) {
122: continue;
123: }
124:
125: $diffSpaces = abs($prevLineSpaces - $lineSpaces);
126: $diffTabs = abs($prevLineTabs - $lineTabs);
127: if (!$diffTabs) {
128: // Skip if the difference could be alignment-related and doesn't
129: // match the file's default indentation
130: if (
131: $diffSpaces
132: && $lineSpaces
133: && $lineSpaces - 1 < strlen($_prevLine)
134: && $_line[$lineSpaces] !== ' '
135: && $_prevLine[$lineSpaces - 1] === ' '
136: && $_prevLine[-1] === ','
137: && !(
138: $default
139: && $default->InsertSpaces
140: && $default->TabSize === $diffSpaces
141: )
142: ) {
143: $prevLine = $_prevLine;
144: $prevOffset = $_prevOffset;
145: continue;
146: }
147: } elseif ($diffSpaces % $diffTabs === 0) {
148: $diffSpaces /= $diffTabs;
149: } else {
150: continue;
151: }
152:
153: if ($diffSpaces > 1 && $diffSpaces <= 8) {
154: $diffSpacesCount[$diffSpaces]++;
155: }
156: }
157:
158: $insertSpaces = $linesWithTabs === $linesWithSpaces
159: ? $default->InsertSpaces ?? true
160: : $linesWithTabs < $linesWithSpaces;
161:
162: $tabSize = $default->TabSize ?? 4;
163:
164: // Only guess tab size if inserting spaces
165: if ($insertSpaces || $alwaysGuessTabSize) {
166: $count = 0;
167: foreach ([2, 4, 6, 8, 3, 5, 7] as $diffSpaces) {
168: if ($diffSpacesCount[$diffSpaces] > $count) {
169: $tabSize = $diffSpaces;
170: $count = $diffSpacesCount[$diffSpaces];
171: }
172: }
173: }
174:
175: if ($close) {
176: File::close($handle, $uri);
177: }
178:
179: if (
180: $default
181: && $default->InsertSpaces === $insertSpaces
182: && $default->TabSize === $tabSize
183: ) {
184: return $default;
185: }
186:
187: return new self($insertSpaces, $tabSize);
188: }
189: }
190: