-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from sqs/add-json-formatter-and-editor
add JSON formatter and editor from VS Code
- Loading branch information
Showing
4 changed files
with
964 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import { ParseError, Node, parseTree, findNodeAtLocation, JSONPath, Segment } from './main'; | ||
import { Edit, FormattingOptions, format, applyEdit } from './format'; | ||
|
||
export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] { | ||
return setProperty(text, path, void 0, formattingOptions); | ||
} | ||
|
||
export function setProperty(text: string, path: JSONPath, value: any, formattingOptions: FormattingOptions, getInsertionIndex?: (properties: string[]) => number): Edit[] { | ||
let errors: ParseError[] = []; | ||
let root = parseTree(text, errors); | ||
let parent: Node = void 0; | ||
|
||
let lastSegment: Segment = void 0; | ||
while (path.length > 0) { | ||
lastSegment = path.pop(); | ||
parent = findNodeAtLocation(root, path); | ||
if (parent === void 0 && value !== void 0) { | ||
if (typeof lastSegment === 'string') { | ||
value = { [lastSegment]: value }; | ||
} else { | ||
value = [value]; | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
if (!parent) { | ||
// empty document | ||
if (value === void 0) { // delete | ||
throw new Error('Can not delete in empty document'); | ||
} | ||
return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, formattingOptions); | ||
} else if (parent.type === 'object' && typeof lastSegment === 'string') { | ||
let existing = findNodeAtLocation(parent, [lastSegment]); | ||
if (existing !== void 0) { | ||
if (value === void 0) { // delete | ||
let propertyIndex = parent.children.indexOf(existing.parent); | ||
let removeBegin: number; | ||
let removeEnd = existing.parent.offset + existing.parent.length; | ||
if (propertyIndex > 0) { | ||
// remove the comma of the previous node | ||
let previous = parent.children[propertyIndex - 1]; | ||
removeBegin = previous.offset + previous.length; | ||
} else { | ||
removeBegin = parent.offset + 1; | ||
if (parent.children.length > 1) { | ||
// remove the comma of the next node | ||
let next = parent.children[1]; | ||
removeEnd = next.offset; | ||
} | ||
} | ||
return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: '' }, formattingOptions); | ||
} else { | ||
// set value of existing property | ||
return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, formattingOptions); | ||
} | ||
} else { | ||
if (value === void 0) { // delete | ||
return []; // property does not exist, nothing to do | ||
} | ||
let newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`; | ||
let index = getInsertionIndex ? getInsertionIndex(parent.children.map(p => p.children[0].value)) : parent.children.length; | ||
let edit: Edit; | ||
if (index > 0) { | ||
let previous = parent.children[index - 1]; | ||
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; | ||
} else if (parent.children.length === 0) { | ||
edit = { offset: parent.offset + 1, length: 0, content: newProperty }; | ||
} else { | ||
edit = { offset: parent.offset + 1, length: 0, content: newProperty + ',' }; | ||
} | ||
return withFormatting(text, edit, formattingOptions); | ||
} | ||
} else if (parent.type === 'array' && typeof lastSegment === 'number') { | ||
let insertIndex = lastSegment; | ||
if (insertIndex === -1) { | ||
// Insert | ||
let newProperty = `${JSON.stringify(value)}`; | ||
let edit: Edit; | ||
if (parent.children.length === 0) { | ||
edit = { offset: parent.offset + 1, length: 0, content: newProperty }; | ||
} else { | ||
let previous = parent.children[parent.children.length - 1]; | ||
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; | ||
} | ||
return withFormatting(text, edit, formattingOptions); | ||
} else { | ||
if (value === void 0 && parent.children.length >= 0) { | ||
//Removal | ||
let removalIndex = lastSegment; | ||
let toRemove = parent.children[removalIndex]; | ||
let edit: Edit; | ||
if (parent.children.length === 1) { | ||
// only item | ||
edit = { offset: parent.offset + 1, length: parent.length - 2, content: '' }; | ||
} else if (parent.children.length - 1 === removalIndex) { | ||
// last item | ||
let previous = parent.children[removalIndex - 1]; | ||
let offset = previous.offset + previous.length; | ||
let parentEndOffset = parent.offset + parent.length; | ||
edit = { offset, length: parentEndOffset - 2 - offset, content: '' }; | ||
} else { | ||
edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: '' }; | ||
} | ||
return withFormatting(text, edit, formattingOptions); | ||
} else { | ||
throw new Error('Array modification not supported yet'); | ||
} | ||
} | ||
} else { | ||
throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`); | ||
} | ||
} | ||
|
||
function withFormatting(text: string, edit: Edit, formattingOptions: FormattingOptions): Edit[] { | ||
// apply the edit | ||
let newText = applyEdit(text, edit); | ||
|
||
// format the new text | ||
let begin = edit.offset; | ||
let end = edit.offset + edit.content.length; | ||
let edits = format(newText, { offset: begin, length: end - begin }, formattingOptions); | ||
|
||
// apply the formatting edits and track the begin and end offsets of the changes | ||
for (let i = edits.length - 1; i >= 0; i--) { | ||
let edit = edits[i]; | ||
newText = applyEdit(newText, edit); | ||
begin = Math.min(begin, edit.offset); | ||
end = Math.max(end, edit.offset + edit.length); | ||
end += edit.content.length - edit.length; | ||
} | ||
// create a single edit with all changes | ||
let editLength = text.length - (newText.length - end) - begin; | ||
return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import * as Json from './main'; | ||
|
||
export interface FormattingOptions { | ||
/** | ||
* If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent? | ||
*/ | ||
tabSize: number; | ||
/** | ||
* Is indentation based on spaces? | ||
*/ | ||
insertSpaces: boolean; | ||
/** | ||
* The default end of line line character | ||
*/ | ||
eol: string; | ||
} | ||
|
||
export interface Edit { | ||
offset: number; | ||
length: number; | ||
content: string; | ||
} | ||
|
||
export function applyEdit(text: string, edit: Edit): string { | ||
return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length); | ||
} | ||
|
||
export function applyEdits(text: string, edits: Edit[]): string { | ||
for (let i = edits.length - 1; i >= 0; i--) { | ||
text = applyEdit(text, edits[i]); | ||
} | ||
return text; | ||
} | ||
|
||
export function format(documentText: string, range: { offset: number, length: number }, options: FormattingOptions): Edit[] { | ||
let initialIndentLevel: number; | ||
let value: string; | ||
let rangeStart: number; | ||
let rangeEnd: number; | ||
if (range) { | ||
rangeStart = range.offset; | ||
rangeEnd = rangeStart + range.length; | ||
while (rangeStart > 0 && !isEOL(documentText, rangeStart - 1)) { | ||
rangeStart--; | ||
} | ||
let scanner = Json.createScanner(documentText, true); | ||
scanner.setPosition(rangeEnd); | ||
scanner.scan(); | ||
rangeEnd = scanner.getPosition(); | ||
|
||
value = documentText.substring(rangeStart, rangeEnd); | ||
initialIndentLevel = computeIndentLevel(value, 0, options); | ||
} else { | ||
value = documentText; | ||
rangeStart = 0; | ||
rangeEnd = documentText.length; | ||
initialIndentLevel = 0; | ||
} | ||
let eol = getEOL(options, documentText); | ||
|
||
let lineBreak = false; | ||
let indentLevel = 0; | ||
let indentValue: string; | ||
if (options.insertSpaces) { | ||
indentValue = repeat(' ', options.tabSize); | ||
} else { | ||
indentValue = '\t'; | ||
} | ||
|
||
let scanner = Json.createScanner(value, false); | ||
|
||
function newLineAndIndent(): string { | ||
return eol + repeat(indentValue, initialIndentLevel + indentLevel); | ||
} | ||
function scanNext(): Json.SyntaxKind { | ||
let token = scanner.scan(); | ||
lineBreak = false; | ||
while (token === Json.SyntaxKind.Trivia || token === Json.SyntaxKind.LineBreakTrivia) { | ||
lineBreak = lineBreak || (token === Json.SyntaxKind.LineBreakTrivia); | ||
token = scanner.scan(); | ||
} | ||
return token; | ||
} | ||
let editOperations: Edit[] = []; | ||
function addEdit(text: string, startOffset: number, endOffset: number) { | ||
if (documentText.substring(startOffset, endOffset) !== text) { | ||
editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text }); | ||
} | ||
} | ||
|
||
let firstToken = scanNext(); | ||
if (firstToken !== Json.SyntaxKind.EOF) { | ||
let firstTokenStart = scanner.getTokenOffset() + rangeStart; | ||
let initialIndent = repeat(indentValue, initialIndentLevel); | ||
addEdit(initialIndent, rangeStart, firstTokenStart); | ||
} | ||
|
||
while (firstToken !== Json.SyntaxKind.EOF) { | ||
let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + rangeStart; | ||
let secondToken = scanNext(); | ||
|
||
let replaceContent = ''; | ||
while (!lineBreak && (secondToken === Json.SyntaxKind.LineCommentTrivia || secondToken === Json.SyntaxKind.BlockCommentTrivia)) { | ||
// comments on the same line: keep them on the same line, but ignore them otherwise | ||
let commentTokenStart = scanner.getTokenOffset() + rangeStart; | ||
addEdit(' ', firstTokenEnd, commentTokenStart); | ||
firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + rangeStart; | ||
replaceContent = secondToken === Json.SyntaxKind.LineCommentTrivia ? newLineAndIndent() : ''; | ||
secondToken = scanNext(); | ||
} | ||
|
||
if (secondToken === Json.SyntaxKind.CloseBraceToken) { | ||
if (firstToken !== Json.SyntaxKind.OpenBraceToken) { | ||
indentLevel--; | ||
replaceContent = newLineAndIndent(); | ||
} | ||
} else if (secondToken === Json.SyntaxKind.CloseBracketToken) { | ||
if (firstToken !== Json.SyntaxKind.OpenBracketToken) { | ||
indentLevel--; | ||
replaceContent = newLineAndIndent(); | ||
} | ||
} else if (secondToken !== Json.SyntaxKind.EOF) { | ||
switch (firstToken) { | ||
case Json.SyntaxKind.OpenBracketToken: | ||
case Json.SyntaxKind.OpenBraceToken: | ||
indentLevel++; | ||
replaceContent = newLineAndIndent(); | ||
break; | ||
case Json.SyntaxKind.CommaToken: | ||
case Json.SyntaxKind.LineCommentTrivia: | ||
replaceContent = newLineAndIndent(); | ||
break; | ||
case Json.SyntaxKind.BlockCommentTrivia: | ||
if (lineBreak) { | ||
replaceContent = newLineAndIndent(); | ||
} else { | ||
// symbol following comment on the same line: keep on same line, separate with ' ' | ||
replaceContent = ' '; | ||
} | ||
break; | ||
case Json.SyntaxKind.ColonToken: | ||
replaceContent = ' '; | ||
break; | ||
case Json.SyntaxKind.NullKeyword: | ||
case Json.SyntaxKind.TrueKeyword: | ||
case Json.SyntaxKind.FalseKeyword: | ||
case Json.SyntaxKind.NumericLiteral: | ||
if (secondToken === Json.SyntaxKind.NullKeyword || secondToken === Json.SyntaxKind.FalseKeyword || secondToken === Json.SyntaxKind.NumericLiteral) { | ||
replaceContent = ' '; | ||
} | ||
break; | ||
} | ||
if (lineBreak && (secondToken === Json.SyntaxKind.LineCommentTrivia || secondToken === Json.SyntaxKind.BlockCommentTrivia)) { | ||
replaceContent = newLineAndIndent(); | ||
} | ||
|
||
} | ||
let secondTokenStart = scanner.getTokenOffset() + rangeStart; | ||
addEdit(replaceContent, firstTokenEnd, secondTokenStart); | ||
firstToken = secondToken; | ||
} | ||
return editOperations; | ||
} | ||
|
||
function repeat(s: string, count: number): string { | ||
let result = ''; | ||
for (let i = 0; i < count; i++) { | ||
result += s; | ||
} | ||
return result; | ||
} | ||
|
||
function computeIndentLevel(content: string, offset: number, options: FormattingOptions): number { | ||
let i = 0; | ||
let nChars = 0; | ||
let tabSize = options.tabSize || 4; | ||
while (i < content.length) { | ||
let ch = content.charAt(i); | ||
if (ch === ' ') { | ||
nChars++; | ||
} else if (ch === '\t') { | ||
nChars += tabSize; | ||
} else { | ||
break; | ||
} | ||
i++; | ||
} | ||
return Math.floor(nChars / tabSize); | ||
} | ||
|
||
function getEOL(options: FormattingOptions, text: string): string { | ||
for (let i = 0; i < text.length; i++) { | ||
let ch = text.charAt(i); | ||
if (ch === '\r') { | ||
if (i + 1 < text.length && text.charAt(i + 1) === '\n') { | ||
return '\r\n'; | ||
} | ||
return '\r'; | ||
} else if (ch === '\n') { | ||
return '\n'; | ||
} | ||
} | ||
return (options && options.eol) || '\n'; | ||
} | ||
|
||
function isEOL(text: string, offset: number) { | ||
return '\r\n'.indexOf(text.charAt(offset)) !== -1; | ||
} |
Oops, something went wrong.