Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/jsonFormatter.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { createScanner, ScanError, SyntaxKind } from './json.js';
7
8
export interface FormattingOptions {
9
/**
10
* If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent?
11
*/
12
tabSize?: number;
13
/**
14
* Is indentation based on spaces?
15
*/
16
insertSpaces?: boolean;
17
/**
18
* The default 'end of line' character. If not set, '\n' is used as default.
19
*/
20
eol?: string;
21
}
22
23
/**
24
* Represents a text modification
25
*/
26
export interface Edit {
27
/**
28
* The start offset of the modification.
29
*/
30
offset: number;
31
/**
32
* The length of the modification. Must not be negative. Empty length represents an *insert*.
33
*/
34
length: number;
35
/**
36
* The new content. Empty content represents a *remove*.
37
*/
38
content: string;
39
}
40
41
/**
42
* A text range in the document
43
*/
44
export interface Range {
45
/**
46
* The start offset of the range.
47
*/
48
offset: number;
49
/**
50
* The length of the range. Must not be negative.
51
*/
52
length: number;
53
}
54
55
56
export function format(documentText: string, range: Range | undefined, options: FormattingOptions): Edit[] {
57
let initialIndentLevel: number;
58
let formatText: string;
59
let formatTextStart: number;
60
let rangeStart: number;
61
let rangeEnd: number;
62
if (range) {
63
rangeStart = range.offset;
64
rangeEnd = rangeStart + range.length;
65
66
formatTextStart = rangeStart;
67
while (formatTextStart > 0 && !isEOL(documentText, formatTextStart - 1)) {
68
formatTextStart--;
69
}
70
let endOffset = rangeEnd;
71
while (endOffset < documentText.length && !isEOL(documentText, endOffset)) {
72
endOffset++;
73
}
74
formatText = documentText.substring(formatTextStart, endOffset);
75
initialIndentLevel = computeIndentLevel(formatText, options);
76
} else {
77
formatText = documentText;
78
initialIndentLevel = 0;
79
formatTextStart = 0;
80
rangeStart = 0;
81
rangeEnd = documentText.length;
82
}
83
const eol = getEOL(options, documentText);
84
85
let lineBreak = false;
86
let indentLevel = 0;
87
let indentValue: string;
88
if (options.insertSpaces) {
89
indentValue = repeat(' ', options.tabSize || 4);
90
} else {
91
indentValue = '\t';
92
}
93
94
const scanner = createScanner(formatText, false);
95
let hasError = false;
96
97
function newLineAndIndent(): string {
98
return eol + repeat(indentValue, initialIndentLevel + indentLevel);
99
}
100
function scanNext(): SyntaxKind {
101
let token = scanner.scan();
102
lineBreak = false;
103
while (token === SyntaxKind.Trivia || token === SyntaxKind.LineBreakTrivia) {
104
lineBreak = lineBreak || (token === SyntaxKind.LineBreakTrivia);
105
token = scanner.scan();
106
}
107
hasError = token === SyntaxKind.Unknown || scanner.getTokenError() !== ScanError.None;
108
return token;
109
}
110
const editOperations: Edit[] = [];
111
function addEdit(text: string, startOffset: number, endOffset: number) {
112
if (!hasError && startOffset < rangeEnd && endOffset > rangeStart && documentText.substring(startOffset, endOffset) !== text) {
113
editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text });
114
}
115
}
116
117
let firstToken = scanNext();
118
119
if (firstToken !== SyntaxKind.EOF) {
120
const firstTokenStart = scanner.getTokenOffset() + formatTextStart;
121
const initialIndent = repeat(indentValue, initialIndentLevel);
122
addEdit(initialIndent, formatTextStart, firstTokenStart);
123
}
124
125
while (firstToken !== SyntaxKind.EOF) {
126
let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
127
let secondToken = scanNext();
128
129
let replaceContent = '';
130
while (!lineBreak && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) {
131
// comments on the same line: keep them on the same line, but ignore them otherwise
132
const commentTokenStart = scanner.getTokenOffset() + formatTextStart;
133
addEdit(' ', firstTokenEnd, commentTokenStart);
134
firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
135
replaceContent = secondToken === SyntaxKind.LineCommentTrivia ? newLineAndIndent() : '';
136
secondToken = scanNext();
137
}
138
139
if (secondToken === SyntaxKind.CloseBraceToken) {
140
if (firstToken !== SyntaxKind.OpenBraceToken) {
141
indentLevel--;
142
replaceContent = newLineAndIndent();
143
}
144
} else if (secondToken === SyntaxKind.CloseBracketToken) {
145
if (firstToken !== SyntaxKind.OpenBracketToken) {
146
indentLevel--;
147
replaceContent = newLineAndIndent();
148
}
149
} else {
150
switch (firstToken) {
151
case SyntaxKind.OpenBracketToken:
152
case SyntaxKind.OpenBraceToken:
153
indentLevel++;
154
replaceContent = newLineAndIndent();
155
break;
156
case SyntaxKind.CommaToken:
157
case SyntaxKind.LineCommentTrivia:
158
replaceContent = newLineAndIndent();
159
break;
160
case SyntaxKind.BlockCommentTrivia:
161
if (lineBreak) {
162
replaceContent = newLineAndIndent();
163
} else {
164
// symbol following comment on the same line: keep on same line, separate with ' '
165
replaceContent = ' ';
166
}
167
break;
168
case SyntaxKind.ColonToken:
169
replaceContent = ' ';
170
break;
171
case SyntaxKind.StringLiteral:
172
if (secondToken === SyntaxKind.ColonToken) {
173
replaceContent = '';
174
break;
175
}
176
// fall through
177
case SyntaxKind.NullKeyword:
178
case SyntaxKind.TrueKeyword:
179
case SyntaxKind.FalseKeyword:
180
case SyntaxKind.NumericLiteral:
181
case SyntaxKind.CloseBraceToken:
182
case SyntaxKind.CloseBracketToken:
183
if (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia) {
184
replaceContent = ' ';
185
} else if (secondToken !== SyntaxKind.CommaToken && secondToken !== SyntaxKind.EOF) {
186
hasError = true;
187
}
188
break;
189
case SyntaxKind.Unknown:
190
hasError = true;
191
break;
192
}
193
if (lineBreak && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) {
194
replaceContent = newLineAndIndent();
195
}
196
197
}
198
const secondTokenStart = scanner.getTokenOffset() + formatTextStart;
199
addEdit(replaceContent, firstTokenEnd, secondTokenStart);
200
firstToken = secondToken;
201
}
202
return editOperations;
203
}
204
205
/**
206
* Creates a formatted string out of the object passed as argument, using the given formatting options
207
* @param any The object to stringify and format
208
* @param options The formatting options to use
209
*/
210
export function toFormattedString(obj: unknown, options: FormattingOptions) {
211
const content = JSON.stringify(obj, undefined, options.insertSpaces ? options.tabSize || 4 : '\t');
212
if (options.eol !== undefined) {
213
return content.replace(/\r\n|\r|\n/g, options.eol);
214
}
215
return content;
216
}
217
218
function repeat(s: string, count: number): string {
219
let result = '';
220
for (let i = 0; i < count; i++) {
221
result += s;
222
}
223
return result;
224
}
225
226
function computeIndentLevel(content: string, options: FormattingOptions): number {
227
let i = 0;
228
let nChars = 0;
229
const tabSize = options.tabSize || 4;
230
while (i < content.length) {
231
const ch = content.charAt(i);
232
if (ch === ' ') {
233
nChars++;
234
} else if (ch === '\t') {
235
nChars += tabSize;
236
} else {
237
break;
238
}
239
i++;
240
}
241
return Math.floor(nChars / tabSize);
242
}
243
244
export function getEOL(options: FormattingOptions, text: string): string {
245
for (let i = 0; i < text.length; i++) {
246
const ch = text.charAt(i);
247
if (ch === '\r') {
248
if (i + 1 < text.length && text.charAt(i + 1) === '\n') {
249
return '\r\n';
250
}
251
return '\r';
252
} else if (ch === '\n') {
253
return '\n';
254
}
255
}
256
return (options && options.eol) || '\n';
257
}
258
259
export function isEOL(text: string, offset: number) {
260
return '\r\n'.indexOf(text.charAt(offset)) !== -1;
261
}
262
263