Skip to content

Commit

Permalink
[skip ci] feat: Move to CodeActions model (#8)
Browse files Browse the repository at this point in the history
Closes #7 
BREAKING! Settings `runOnSave` and `runFormatter` removed. Now you need setup `codeActionsOnSave` to make it work on save, like so:
```json
"editor.codeActionsOnSave": {
  "source.fixAll": true,
}
```
See linked issue for motivation
  • Loading branch information
zardoy authored Nov 22, 2022
1 parent 9161c4f commit 5f509a5
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 35 deletions.
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,9 @@
},
"additionalProperties": false
},
"runFormatter": {
"type": "boolean",
"default": true
},
"runOnSave": {
"enableIndividualCodeActions": {
"type": "boolean",
"description": "Wether to enable all types of code fixes for individual problems",
"default": true
}
}
Expand Down
115 changes: 87 additions & 28 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,39 @@ export const activate = () => {
},
)

const performFixes = async () => {
const currentEditor = vscode.window.activeTextEditor
if (currentEditor === undefined || currentEditor.viewColumn === undefined) return

const { document } = currentEditor
// TODO test jsonc
if (document.isClosed || !['json', 'jsonc'].includes(document.languageId)) return

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

if (diagnostics.length === 0) {
console.warn('Everything is clean')
return
const getJsonFixes = (
document: vscode.TextDocument,
diagnostics: readonly vscode.Diagnostic[],
isSingleCodeActionFix = false,
): { workspaceEdit: vscode.WorkspaceEdit; titleOverride?: string } | void => {
const enableFixes = isSingleCodeActionFix
? new Proxy({} as never, {
get(target, p, receiver) {
return true
},
})
: getExtensionSetting('enableFixes')
const edits: vscode.TextEdit[] = []
const editCallbackBuilder = (cb: (edit: Pick<vscode.TextEditorEdit, 'insert' | 'delete'>) => void) => {
cb({
insert(pos, value) {
edits.push({
range: new vscode.Range(pos, pos),
newText: value,
})
},
delete(range) {
edits.push({
range,
newText: '',
})
},
})
}

console.time('process')
const enableFixes = getExtensionSetting('enableFixes')
let needsFormatter = false
await currentEditor.edit(edit => {
let codeActionTitleOverride: string | undefined

editCallbackBuilder(edit => {
for (const problem of diagnostics) {
const { line, character } = problem.range.start

Expand All @@ -52,7 +66,8 @@ export const activate = () => {
// }

edit.insert(pos, ',')
needsFormatter = true
codeActionTitleOverride = 'Insert comma'
// needsFormatter = true
break
}

Expand All @@ -61,19 +76,22 @@ export const activate = () => {
case 'Trailing comma':
if (!enableFixes.removeTrailingCommas) continue
edit.delete(problem.range)
codeActionTitleOverride = 'Remove trailing comma'
break

// 515
case 'Colon expected':
if (!enableFixes.insertMissingColon) continue
edit.insert(pos, ':')
needsFormatter = true
codeActionTitleOverride = 'Insert colon'
// needsFormatter = true
break
// why no source and code?
case 'Comments are not permitted in JSON.':
if (!enableFixes.removeComments) continue
edit.delete(problem.range)
needsFormatter = true
codeActionTitleOverride = 'Remove comment'
// needsFormatter = true
break
case 'Property keys must be doublequoted': {
if (!enableFixes.fixDoubleQuotes) continue
Expand All @@ -89,7 +107,8 @@ export const activate = () => {

edit.insert(new vscode.Position(start.line, start.character), '"')
edit.insert(new vscode.Position(end.line, end.character), '"')
needsFormatter = true
codeActionTitleOverride = 'Wrap with double quotes'
// needsFormatter = true
break
}

Expand All @@ -98,15 +117,55 @@ export const activate = () => {
}
}
})
// we're actually running formatter twice (before and after this)
if (needsFormatter && getExtensionSetting('runFormatter')) await vscode.commands.executeCommand('editor.action.formatDocument')
console.timeEnd('process')

if (edits.length === 0) return

const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.set(document.uri, edits)

return {
workspaceEdit,
titleOverride: codeActionTitleOverride,
}
}

registerExtensionCommand('fixFile', async () => performFixes())
vscode.languages.registerCodeActionsProvider(['json', 'jsonc'], {
provideCodeActions(document, range, context) {
const fixAllRequest = context.only?.contains(vscode.CodeActionKind.SourceFixAll.append('source.fixAll.eslint'))
if (!fixAllRequest) {
if (!getExtensionSetting('enableIndividualCodeActions')) return
// ensure propose one individual fix
const firstDiagnostic = context.diagnostics[0]
if (!firstDiagnostic) return
const fix = getJsonFixes(document, [firstDiagnostic], true)
if (!fix) return
return [
{
title: fix.titleOverride ?? `Fix ${firstDiagnostic.message}`,
kind: vscode.CodeActionKind.QuickFix,
edit: fix.workspaceEdit,
},
]
}

const { workspaceEdit } = getJsonFixes(document, context.diagnostics) ?? {}
if (!workspaceEdit) return
return [
{
title: 'Fix all JSON problems',
// diagnostics
edit: workspaceEdit,
kind: vscode.CodeActionKind.SourceFixAll,
},
]
},
})

vscode.workspace.onWillSaveTextDocument(({ waitUntil, reason }) => {
if (!getExtensionSetting('runOnSave') || reason === vscode.TextDocumentSaveReason.AfterDelay) return
waitUntil(performFixes())
registerExtensionCommand('fixFile', async () => {
const editor = vscode.window.activeTextEditor
if (!editor) return
const { workspaceEdit } = getJsonFixes(editor.document, vscode.languages.getDiagnostics(editor.document.uri)) ?? {}
if (!workspaceEdit) return
await vscode.workspace.applyEdit(workspaceEdit)
})
}
11 changes: 9 additions & 2 deletions test/integration/suite/jsonFixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from 'vscode'

import { expect } from 'chai'
// import delay from 'delay'
import { clearEditorText, setupFixtureContent } from './utils'
import { setupFixtureContent } from './utils'
import { jsonFixesFixtures } from '../fixtures/files'
import dedent from 'string-dedent'
import { join } from 'path'
Expand All @@ -12,13 +12,16 @@ describe('Json Fixes', () => {
let document: vscode.TextDocument
let editor: vscode.TextEditor
let temporaryFile = join(__dirname, '../temp.json')
if (!fs.existsSync(temporaryFile)) fs.writeFileSync(temporaryFile, '', 'utf8')
fs.writeFileSync(temporaryFile, '', 'utf8')
before(done => {
void vscode.window
.showTextDocument(vscode.Uri.file(temporaryFile))
.then(async newEditor => {
editor = newEditor
document = editor.document
await vscode.workspace.getConfiguration('').update('editor.codeActionsOnSave', { 'source.fixAll': true }, vscode.ConfigurationTarget.Global)
await vscode.workspace.getConfiguration('').update('editor.formatOnSave', true, vscode.ConfigurationTarget.Global)
await vscode.workspace.getConfiguration('').update('files.eol', '\n', vscode.ConfigurationTarget.Global)
})
.then(done)
})
Expand All @@ -30,6 +33,10 @@ describe('Json Fixes', () => {
})
})
await Promise.all([setupFixtureContent(editor, content.input), diagnosticsChangePromise])
console.log(
'[debug] diagnostics:',
vscode.languages.getDiagnostics(document.uri).map(({ message }) => message),
)
await document.save()
expect(document.getText()).to.equal(dedent(content.expected))
})
Expand Down

0 comments on commit 5f509a5

Please sign in to comment.