Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add JSON formatter and editor from VS Code #5

Merged
merged 1 commit into from
Jan 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions src/edit.ts
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) }];
}
214 changes: 214 additions & 0 deletions src/format.ts
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;
}
Loading