Skip to content

feat: Insert double quotes on colon #11

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

Merged
merged 10 commits into from
Nov 25, 2022
Merged
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
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ Note 2: this extension though doesn't do value *autoquoting*, there is [vscode-f
### On-Type Fixes

- Insert missing comma on `Enter` (or `Ctrl+Enter`, enabled by default with `fixAllJson.insertMissingCommaOnEnter`)
<!-- - Insert missing double quotes on `:` (enabled by default with `fixAllJson.insertMissingDoubleQuotesOnComma`) -->
- Insert missing double quotes on `:` (enabled by default with `fixAllJson.insertMissingDoubleQuotesOnColon`)
<!-- - Replace `"` with `\"` within strings (disabled by default with `fixAllJson...`) -->
<!-- - Replace `Enter` with `\n` within strings (disabled by default with `fixAllJson...`) -->
<!-- - Replace `Tab` with `\t` within strings (disabled by default with `fixAllJson...`) -->
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -69,6 +69,11 @@
"type": "boolean",
"default": true,
"markdownDescription": "Insert missing comma after \"Enter\". Works only if JSON line ends with `}`, `]`, `\"`, `true`/`false` or number"
},
"insertMissingDoubleQuotesOnColon": {
"type": "boolean",
"default": true,
"markdownDescription": "Insert missing double quotes after \":\""
}
}
}
@@ -98,7 +103,7 @@
"start-web": "vscode-framework start --web",
"build": "tsc && vscode-framework build",
"lint": "eslint src/**",
"integration-test": "rimraf -rf ./testsOut/ && tsc -p tsconfig.test.json && node testsOut/runTests.js",
"integration-test": "rimraf -rf ./testsOut/ .vscode-test/user-data/User/settings.json && tsc -p tsconfig.test.json && node testsOut/runTests.js",
"integration-test:watch": "chokidar \"test/integration/**\" -c \"pnpm integration-test\" --initial"
},
"pnpm": {
20 changes: 2 additions & 18 deletions src/commaOnEnter.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
import * as vscode from 'vscode'
import stripJsonComments from 'strip-json-comments'
import { oneOf } from '@zardoy/utils'
import { getExtensionSetting } from 'vscode-framework'
import { getTextByLine, isEoL, isNumber, startsWithComment } from './utils'
import { getTextByLine, isEoL, isNumber, onJsonFileChange, startsWithComment } from './utils'

export default () => {
vscode.workspace.onDidChangeTextDocument(({ contentChanges, document, reason }) => {
onJsonFileChange(({ contentChanges, editor, document }) => {
if (!getExtensionSetting('insertMissingCommaOnEnter')) return
if (!vscode.languages.match(['json', 'jsonc'], document) || contentChanges.length === 0) return

const editor = vscode.window.activeTextEditor

contentChanges = [...contentChanges].sort((a, b) => a.range.start.compareTo(b.range.start))

if (
document.uri !== editor?.document.uri ||
['output'].includes(editor.document.uri.scheme) ||
vscode.workspace.fs.isWritableFileSystem(document.uri.scheme) === false ||
oneOf(reason, vscode.TextDocumentChangeReason.Undo, vscode.TextDocumentChangeReason.Redo)
) {
return
}

if (contentChanges.some(change => !isEoL(change.text))) return

let fileContentWithoutComments: string
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from 'vscode'
import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework'
import registerCommaOnEnter from './commaOnEnter'
import registerQuotesOnColon from './quotesOnColon'

export const activate = () => {
vscode.languages.registerCodeActionsProvider(
@@ -169,4 +170,5 @@ export const activate = () => {
})

registerCommaOnEnter()
registerQuotesOnColon()
}
46 changes: 46 additions & 0 deletions src/quotesOnColon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as vscode from 'vscode'
import { getExtensionSetting } from 'vscode-framework'
import { onJsonFileChange } from './utils'

export default () => {
onJsonFileChange(({ contentChanges, document, editor }) => {
if (!getExtensionSetting('insertMissingDoubleQuotesOnColon')) return

const diagnostics = vscode.languages.getDiagnostics(document.uri)

if (diagnostics.length === 0) return
if (contentChanges.some(change => change.text !== ':')) return

void editor.edit(async edit => {
let previousLine: number | undefined
let sameLineChanges = 0
const translatePos = (pos: vscode.Position) => pos.translate(0, sameLineChanges - 1)
for (const change of contentChanges) {
const changePos = change.range.end
const changePosLine = changePos.line

if (previousLine === undefined || changePosLine === previousLine) {
sameLineChanges++
} else {
// reset on next line
sameLineChanges = 1
}

previousLine = changePosLine

const problem = diagnostics.find(
diagnostic => diagnostic.range.end.isEqual(changePos) && diagnostic.message === 'Property keys must be doublequoted',
)

if (!problem) continue

const { range } = problem
const start = translatePos(range.start)
const end = translatePos(range.end)

edit.insert(start, '"')
edit.insert(end, '"')
}
})
})
}
26 changes: 26 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as vscode from 'vscode'
import { oneOf } from '@zardoy/utils'

export function getTextByLine(text: string, line: number) {
return text.split('\n').at(line)
}
@@ -13,3 +16,26 @@ export function isEoL(text: string) {
export function startsWithComment(text: string) {
return text.startsWith('//') || text.startsWith('/*')
}

export const onJsonFileChange = (
callback: (input: { editor: vscode.TextEditor } & Pick<vscode.TextDocumentChangeEvent, 'contentChanges' | 'document'>) => void | Promise<void>,
) => {
vscode.workspace.onDidChangeTextDocument(({ contentChanges, document, reason }) => {
if (!vscode.languages.match(['json', 'jsonc'], document) || contentChanges.length === 0) return

const editor = vscode.window.activeTextEditor

if (
document.uri !== editor?.document.uri ||
['output'].includes(editor.document.uri.scheme) ||
vscode.workspace.fs.isWritableFileSystem(document.uri.scheme) === false ||
oneOf(reason, vscode.TextDocumentChangeReason.Undo, vscode.TextDocumentChangeReason.Redo)
) {
return
}

contentChanges = [...contentChanges].sort((a, b) => a.range.start.compareTo(b.range.start))

void callback({ editor, document, contentChanges })
})
}
14 changes: 12 additions & 2 deletions test/integration/suite/commaOnEnter.test.ts
Original file line number Diff line number Diff line change
@@ -49,8 +49,8 @@ describe('Comma on Enter', () => {
.then(done)
})

const testPosition = (num: number, offset: number, isExpected: boolean) => {
it(`${isExpected ? 'Valid' : 'Invalid'} position ${num}`, async () => {
const testPosition = (num: number, offset: number, isExpected: boolean, testFn: any = it) => {
testFn(`${isExpected ? 'Valid' : 'Invalid'} position ${num}`, async () => {
await clearEditorText(editor, FULL_FIXTURE_CONTENT)
const pos = offsetToPosition(FULL_FIXTURE_CONTENT, offset)
editor.selection = new vscode.Selection(pos, pos)
@@ -91,4 +91,14 @@ describe('Comma on Enter', () => {
for (const [i, invalidPosition] of FIXTURE_POSITIONS['/*$*/'].entries()) {
testPosition(i, invalidPosition, false)
}

it('Extension setting disabled', async () => {
await new Promise<void>(async resolve => {
await vscode.workspace.getConfiguration().update('fixAllJson.insertMissingCommaOnEnter', false, vscode.ConfigurationTarget.Global)
testPosition(-1, FIXTURE_POSITIONS['/*|*/'][0]!, false, async (name, cb) => {
await cb()
resolve()
})
})
})
})
11 changes: 2 additions & 9 deletions test/integration/suite/jsonFixes.test.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode'

import { expect } from 'chai'
// import delay from 'delay'
import { getTextNormalizedEol, setupFixtureContent } from './utils'
import { getTextNormalizedEol, setupFixtureContent, waitForJsonDiagnostics } from './utils'
import { jsonFixesFixtures } from '../fixtures/files'
import dedent from 'string-dedent'
import { join } from 'path'
@@ -26,14 +26,7 @@ describe('Json Fixes', () => {
})
for (const [name, content] of Object.entries(jsonFixesFixtures)) {
it(`Fix JSON issues: ${name}`, async () => {
const diagnosticsChangePromise = new Promise<void>(resolve => {
vscode.languages.onDidChangeDiagnostics(({ uris }) => {
if (!uris.map(uri => uri.toString()).includes(document.uri.toString())) return
if (vscode.languages.getDiagnostics(document.uri).length === 0) return
resolve()
})
})
await Promise.all([setupFixtureContent(editor, content.input), diagnosticsChangePromise])
await Promise.all([setupFixtureContent(editor, content.input), waitForJsonDiagnostics(document)])
console.log(
'[debug] diagnostics:',
vscode.languages.getDiagnostics(document.uri).map(({ message }) => message),
75 changes: 75 additions & 0 deletions test/integration/suite/quotesOnColon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as vscode from 'vscode'

import { expect } from 'chai'
import { clearEditorText, waitForJsonDiagnostics } from './utils'

// todo enable back when https://github.com/microsoft/vscode-languageserver-node/issues/1128 is available
describe.skip('Quotes on Colon', () => {
let document: vscode.TextDocument
let editor: vscode.TextEditor

const TYPE_CONTENT = 'key1:'
const FULL_FIXTURE_EXPECTED = '{"key1":, "key1":,\n"key1":}'
const FULL_FIXTURE_UNCHANGED = '{key1:, key1:,\nkey1:}'

before(done => {
void vscode.workspace
.openTextDocument({
content: '',
language: 'jsonc',
})
.then(async newDocument => {
document = newDocument
editor = await vscode.window.showTextDocument(document)
if (process.env.CI) {
await new Promise(resolve => {
setTimeout(resolve, 1000)
})
}
// make sure JSON server is started and diagnostics are here
await Promise.all([clearEditorText(editor, '{'), waitForJsonDiagnostics(document)])
})
.then(done)
})

const typeSequence = async (seq: string) => {
for (const letter of seq) await vscode.commands.executeCommand('type', { text: letter })
}

const execTest = (title: string | undefined, isExpected: boolean) => {
const cb = async () => {
await clearEditorText(editor, '{, ,\n}')
const pos = new vscode.Position(0, 1)
const multiCursorPositions = [pos, pos.translate(0, 2), new vscode.Position(1, 0)]
editor.selections = multiCursorPositions.map(pos => new vscode.Selection(pos, pos))
await typeSequence(TYPE_CONTENT)
if (isExpected) {
// wait for expected changes to be done
await new Promise<void>(resolve => {
const { dispose } = vscode.workspace.onDidChangeTextDocument(({ document: changedDocument }) => {
if (changedDocument !== document) return
dispose()
resolve()
})
})
expect(document.getText()).to.equal(FULL_FIXTURE_EXPECTED)
} else {
// todo would be better to remove timeout in favor of cleaner solution
await new Promise(resolve => {
setTimeout(resolve, 60)
})
expect(document.getText()).to.equal(TYPE_CONTENT)
}
}
if (title) it(title, cb)
else return cb()
return
}

execTest('Double quotes basic case', true)

it('Extension setting disabled', async () => {
await vscode.workspace.getConfiguration().update('fixAllJson.insertMissingDoubleQuotesOnColon', false, vscode.ConfigurationTarget.Global)
await execTest('Double quotes basic case', false)
})
})
10 changes: 10 additions & 0 deletions test/integration/suite/utils.ts
Original file line number Diff line number Diff line change
@@ -56,3 +56,13 @@ export const offsetToPosition = (string: string, offset: number) => {
}
return null!
}

export const waitForJsonDiagnostics = (document: vscode.TextDocument) => {
return new Promise<void>(resolve => {
vscode.languages.onDidChangeDiagnostics(({ uris }) => {
if (!uris.map(uri => uri.toString()).includes(document.uri.toString())) return
if (vscode.languages.getDiagnostics(document.uri).length === 0) return
resolve()
})
})
}