├── .husky ├── .gitignore └── pre-push ├── .eslintignore ├── .browserslistrc ├── .gitignore ├── .babelrc.js ├── src ├── types.ts └── index.ts ├── .editorconfig ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── rollup.conf.js ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/src 3 | !/test 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | node >= 10 2 | Chrome >= 55 3 | Firefox >= 53 4 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.lock 4 | /.idea 5 | /dist 6 | /.nyc_output 7 | /coverage 8 | /types 9 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@gera2ld/plaid/config/babelrc-base'), 3 | presets: [ 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | ].filter(Boolean), 8 | }; 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Position } from 'vscode-languageserver-types'; 2 | 3 | export interface IRangeContent { 4 | text: string; 5 | range?: { 6 | start: Position; 7 | end: Position; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "types", 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "react" 11 | }, 12 | "files": [ 13 | "src/index.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@gera2ld/plaid-common-ts/eslint'), 5 | ], 6 | parserOptions: { 7 | project: './tsconfig.json', 8 | }, 9 | rules: { 10 | '@typescript-eslint/indent': ['error', 2, { 11 | ignoredNodes: ['TSTypeParameterInstantiation'] 12 | }], 13 | '@typescript-eslint/interface-name-prefix': 'off', 14 | '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], 15 | 'import/no-cycle': 'off', 16 | 'import/no-extraneous-dependencies': 'off', 17 | 'no-continue': 'off', 18 | 'no-shadow': 'off', 19 | 'object-curly-newline': 'off', 20 | 'prefer-template': 'off', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gerald 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollup.conf.js: -------------------------------------------------------------------------------- 1 | const { getRollupPlugins, getRollupExternal, defaultOptions, rollupMinify } = require('@gera2ld/plaid'); 2 | const pkg = require('./package.json'); 3 | 4 | const DIST = defaultOptions.distDir; 5 | const FILENAME = 'index'; 6 | const BANNER = `/*! ${pkg.name} v${pkg.version} | ${pkg.license} License */`; 7 | 8 | const external = getRollupExternal([ 9 | '@gera2ld/format-json', 10 | 'coc.nvim', 11 | 'json5', 12 | 'vscode-languageserver-types', 13 | ]); 14 | const bundleOptions = { 15 | extend: true, 16 | esModule: false, 17 | }; 18 | const rollupConfig = [ 19 | { 20 | input: { 21 | input: 'src/index.ts', 22 | plugins: getRollupPlugins({ 23 | extensions: defaultOptions.extensions, 24 | }), 25 | external, 26 | }, 27 | output: { 28 | format: 'cjs', 29 | file: `${DIST}/${FILENAME}.common.js`, 30 | }, 31 | }, 32 | ]; 33 | 34 | rollupConfig.forEach((item) => { 35 | item.output = { 36 | indent: false, 37 | // If set to false, circular dependencies and live bindings for external imports won't work 38 | externalLiveBindings: false, 39 | ...item.output, 40 | ...BANNER && { 41 | banner: BANNER, 42 | }, 43 | }; 44 | }); 45 | 46 | module.exports = rollupConfig.map(({ input, output }) => ({ 47 | ...input, 48 | output, 49 | })); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-format-json", 3 | "version": "0.2.3", 4 | "description": "format JSON with the power of coc.nvim", 5 | "author": "Gerald ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "rollup -wc rollup.conf.js", 9 | "clean": "del-cli dist types", 10 | "ci": "npm run lint", 11 | "lint": "eslint --ext .ts .", 12 | "build:types": "tsc", 13 | "build:js": "rollup -c rollup.conf.js", 14 | "build": "run-s ci clean build:types build:js", 15 | "prepublishOnly": "run-s build", 16 | "prepare": "husky install" 17 | }, 18 | "main": "dist/index.common.js", 19 | "files": [ 20 | "dist", 21 | "types" 22 | ], 23 | "publishConfig": { 24 | "access": "public", 25 | "registry": "https://registry.npmjs.org/" 26 | }, 27 | "typings": "types/index.d.ts", 28 | "devDependencies": { 29 | "@gera2ld/plaid": "~2.5.1", 30 | "@gera2ld/plaid-common-ts": "~2.5.1", 31 | "@gera2ld/plaid-rollup": "~2.5.0", 32 | "coc.nvim": "^0.0.80", 33 | "del-cli": "^4.0.1", 34 | "husky": "^7.0.4", 35 | "vscode-languageserver-types": "^3.16.0" 36 | }, 37 | "dependencies": { 38 | "@babel/runtime": "^7.17.7", 39 | "@gera2ld/format-json": "^0.2.5", 40 | "json5": "^2.2.0" 41 | }, 42 | "engines": { 43 | "coc": ">= 0.0.70" 44 | }, 45 | "keywords": [ 46 | "coc.nvim", 47 | "json", 48 | "javascript" 49 | ], 50 | "repository": "git@github.com:gera2ld/coc-format-json.git" 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coc-format-json 2 | 3 | ![NPM](https://img.shields.io/npm/v/coc-format-json.svg) 4 | ![License](https://img.shields.io/npm/l/coc-format-json.svg) 5 | ![Downloads](https://img.shields.io/npm/dt/coc-format-json.svg) 6 | 7 | Format JSON strings on top of [coc.nvim](https://github.com/neoclide/coc.nvim), the JavaScript way. 8 | 9 | ![formatJson](https://user-images.githubusercontent.com/3139113/88083845-1bda5000-cbb6-11ea-9ac3-b50e61de427f.gif) 10 | 11 | ## Installation 12 | 13 | First, make sure [coc.nvim](https://github.com/neoclide/coc.nvim) is started. 14 | 15 | Then install with the Vim command: 16 | 17 | ```viml 18 | :CocInstall coc-format-json 19 | ``` 20 | 21 | ## Usage 22 | 23 | Make sure the text to be serialized is valid JSON or text that can be parsed by [JSON5](https://json5.org/). 24 | 25 | ### Format the whole file 26 | 27 | ```viml 28 | :CocCommand formatJson [options] 29 | ``` 30 | 31 | See [options](#Options) section for more details. 32 | 33 | ### Format selected text 34 | 35 | First make a selection with `v`. Then execute command: 36 | 37 | ```viml 38 | :CocCommand formatJson.selected [options] 39 | ``` 40 | 41 | See [options](#Options) section for more details. 42 | 43 | ## Options 44 | 45 | ### --indent 46 | 47 | `--indent=` 48 | 49 | Set indentation as `` spaces. When `` is zero, the JSON object will be serialized in compact mode, i.e. no extra white spaces will be kept. 50 | 51 | ### --quote 52 | 53 | `--quote='` 54 | 55 | Set the quote character to use, either `'` or `"`. In typical JSON only `"` is allowed, however in JavaScript it is common to use `'` everywhere. 56 | 57 | ### --quote-as-needed 58 | 59 | `--quote-as-needed` or `--quote-as-needed=` 60 | 61 | While enabled, all quotes will be ommitted if possible, if only the serialized text is valid in JavaScript. 62 | 63 | ### --trailing 64 | 65 | `--trailing` or `--trailing=` 66 | 67 | While enabled, an dangling comma will be added to the last entry in each array and object. This is useful in JavaScript so that you don't have to modify the last line before appending a new line. 68 | 69 | ### --template 70 | 71 | `--template` or `--template=` 72 | 73 | While enabled, multiline strings will be serialized as template literals quoted with `` ` ``, instead of using escaped characters like `\n`. 74 | 75 | ### --sort-keys 76 | 77 | `--sort-keys` or `--sort-keys=` 78 | 79 | While enabled, keys of an object will be sorted alphabetically. 80 | 81 | ## Presets 82 | 83 | Some presets are available to set a group of options: 84 | 85 | - `--preset-js` 86 | 87 | useful to use in JavaScript code, equivalent to `--quote=' --quote-as-needed --trailing --template`. 88 | 89 | - `--preset-json` 90 | 91 | useful to use in JSON file, equivalent to `--quote=" --quote-as-needed=false --trailing=false --template=false`. 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtensionContext, 3 | commands, 4 | workspace, 5 | } from 'coc.nvim'; 6 | import { format, FormatJSONData, FormatJSONOptions } from '@gera2ld/format-json'; 7 | import JSON5 from 'json5'; 8 | import { IRangeContent } from './types'; 9 | 10 | const optionsJSON: FormatJSONOptions = { 11 | indent: 2, 12 | quote: '"', 13 | quoteAsNeeded: false, 14 | trailing: false, 15 | template: false, 16 | }; 17 | const optionsJS: FormatJSONOptions = { 18 | indent: 2, 19 | quote: '\'', 20 | quoteAsNeeded: true, 21 | trailing: true, 22 | template: true, 23 | }; 24 | 25 | function keysOriginalOrder(obj: Record) { 26 | return Object.entries(obj); 27 | } 28 | 29 | async function getContent(hasSelection = false): Promise { 30 | const doc = await workspace.document; 31 | const range = await workspace.getSelectedRange('v', doc); 32 | if (hasSelection) { 33 | return { 34 | range, 35 | text: doc.textDocument.getText(range), 36 | }; 37 | } 38 | return { 39 | text: doc.textDocument.getText(), 40 | }; 41 | } 42 | 43 | function getOptions(args: string[]): FormatJSONOptions { 44 | const rawOptions: { [key: string]: string } = {}; 45 | let key: string; 46 | for (const arg of args) { 47 | if (arg.startsWith('--')) { 48 | let value: string; 49 | [key, value] = arg.slice(2).split('='); 50 | key = key.replace(/-(\w)/g, (_m, g) => g.toUpperCase()); 51 | rawOptions[key] = value ?? 'true'; 52 | } else if (key) { 53 | rawOptions[key] = arg; 54 | } 55 | } 56 | let options: FormatJSONOptions; 57 | if (rawOptions.presetJs === 'true') { 58 | options = { ...optionsJS }; 59 | } else { 60 | options = { ...optionsJSON }; 61 | } 62 | if (rawOptions.indent != null) options.indent = +rawOptions.indent; 63 | if (rawOptions.quote === '\'' || rawOptions.quote === '"') options.quote = rawOptions.quote; 64 | if (rawOptions.quoteAsNeeded != null) options.quoteAsNeeded = rawOptions.quoteAsNeeded === 'true'; 65 | if (rawOptions.template != null) options.template = rawOptions.template === 'true'; 66 | if (rawOptions.trailing != null) options.trailing = rawOptions.trailing === 'true'; 67 | if (rawOptions.sortKeys !== 'true') options.entries = keysOriginalOrder; 68 | return options; 69 | } 70 | 71 | async function formatJson(content: IRangeContent, options: FormatJSONOptions): Promise { 72 | const formatted = format(JSON5.parse(content.text), options); 73 | if (content.range) { 74 | const doc = await workspace.document; 75 | doc.applyEdits([{ range: content.range, newText: formatted }]); 76 | } else { 77 | const buffer = await workspace.nvim.buffer; 78 | const [, last] = await workspace.nvim.eval('getpos("$")') as number[]; 79 | buffer.setLines(formatted.split('\n'), { 80 | start: 0, 81 | end: last, 82 | }); 83 | } 84 | } 85 | 86 | export function activate(context: ExtensionContext): void { 87 | context.subscriptions.push(commands.registerCommand( 88 | 'formatJson', 89 | async (...args: string[]) => { 90 | await formatJson(await getContent(), getOptions(args)); 91 | }, 92 | )); 93 | 94 | context.subscriptions.push(commands.registerCommand( 95 | 'formatJson.selected', 96 | async (...args: string[]) => { 97 | await formatJson(await getContent(true), getOptions(args)); 98 | }, 99 | )); 100 | } 101 | --------------------------------------------------------------------------------