Skip to content
Closed
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
59 changes: 59 additions & 0 deletions src/EditorConfigCodeActionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
CodeAction,
CodeActionContext,
CodeActionKind,
CodeActionProvider,
Diagnostic,
EndOfLine,
Range,
Selection,
TextDocument,
WorkspaceEdit,
} from 'vscode'
import {
INLINE_COMMENT_DIAGNOSTIC_CODE,
moveInlineCommentToOwnLine,
} from './inlineComments'

export const MOVE_INLINE_COMMENT_TITLE = 'Move inline comment to its own line'

export default class EditorConfigCodeActionProvider
implements CodeActionProvider
{
public static readonly providedCodeActionKinds = [CodeActionKind.QuickFix]

public provideCodeActions(
document: TextDocument,
_range: Range | Selection,
context: CodeActionContext,
) {
return context.diagnostics
.filter(isInlineCommentDiagnostic)
.flatMap(diagnostic => {
const line = document.lineAt(diagnostic.range.start.line)
const movedComment = moveInlineCommentToOwnLine(
line.text,
document.eol === EndOfLine.CRLF ? '\r\n' : '\n',
)
if (!movedComment) {
return []
}

const edit = new WorkspaceEdit()
edit.replace(document.uri, line.range, movedComment)

const action = new CodeAction(
MOVE_INLINE_COMMENT_TITLE,
CodeActionKind.QuickFix,
)
action.diagnostics = [diagnostic]
action.edit = edit
action.isPreferred = true
return [action]
})
}
}

function isInlineCommentDiagnostic(diagnostic: Diagnostic) {
return diagnostic.code === INLINE_COMMENT_DIAGNOSTIC_CODE
}
81 changes: 81 additions & 0 deletions src/EditorConfigDiagnosticsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
Diagnostic,
DiagnosticCollection,
DiagnosticSeverity,
Disposable,
languages,
Range,
TextDocument,
workspace,
} from 'vscode'
import {
findInlineComment,
INLINE_COMMENT_DIAGNOSTIC_CODE,
INLINE_COMMENT_DIAGNOSTIC_MESSAGE,
} from './inlineComments'

export default class EditorConfigDiagnosticsProvider {
private collection: DiagnosticCollection
private disposable: Disposable

public constructor() {
this.collection = languages.createDiagnosticCollection('editorconfig')

const subscriptions: Disposable[] = []

// Analyze documents that are already open when the extension activates
for (const doc of workspace.textDocuments) {
this.analyze(doc)
}

subscriptions.push(
workspace.onDidOpenTextDocument(doc => this.analyze(doc)),
)
subscriptions.push(
workspace.onDidChangeTextDocument(e => this.analyze(e.document)),
)
subscriptions.push(
workspace.onDidCloseTextDocument(doc =>
this.collection.delete(doc.uri),
),
)

this.disposable = Disposable.from(...subscriptions)
}

private analyze(doc: TextDocument) {
if (doc.languageId !== 'editorconfig') {
return
}

const diagnostics: Diagnostic[] = []

for (let i = 0; i < doc.lineCount; i++) {
const { text } = doc.lineAt(i)
const inlineComment = findInlineComment(text)
if (inlineComment) {
const range = new Range(
i,
inlineComment.commentStart,
i,
text.length,
)
const diagnostic = new Diagnostic(
range,
INLINE_COMMENT_DIAGNOSTIC_MESSAGE,
DiagnosticSeverity.Warning,
)
diagnostic.code = INLINE_COMMENT_DIAGNOSTIC_CODE
diagnostic.source = 'editorconfig'
diagnostics.push(diagnostic)
}
}

this.collection.set(doc.uri, diagnostics)
}

public dispose() {
this.collection.dispose()
this.disposable.dispose()
}
}
18 changes: 17 additions & 1 deletion src/editorConfigMain.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { commands, DocumentSelector, ExtensionContext, languages } from 'vscode'
import {
CodeActionKind,
commands,
DocumentSelector,
ExtensionContext,
languages,
} from 'vscode'
import {
applyTextEditorOptions,
fromEditorConfig,
resolveCoreConfig,
resolveTextEditorOptions,
toEditorConfig,
} from './api'
import EditorConfigCodeActionProvider from './EditorConfigCodeActionProvider'
import { generateEditorConfig } from './commands/generateEditorConfig'
import DocumentWatcher from './DocumentWatcher'
import EditorConfigCompletionProvider from './EditorConfigCompletionProvider'
import EditorConfigDiagnosticsProvider from './EditorConfigDiagnosticsProvider'

/**
* Main entry
*/
export function activate(ctx: ExtensionContext) {
ctx.subscriptions.push(new DocumentWatcher())
ctx.subscriptions.push(new EditorConfigDiagnosticsProvider())

// register .editorconfig file completion provider
const editorConfigFileSelector: DocumentSelector = {
Expand All @@ -26,6 +35,13 @@ export function activate(ctx: ExtensionContext) {
editorConfigFileSelector,
new EditorConfigCompletionProvider(),
)
languages.registerCodeActionsProvider(
{ language: 'editorconfig' },
new EditorConfigCodeActionProvider(),
{
providedCodeActionKinds: [CodeActionKind.QuickFix],
},
)

// register an internal command used to automatically display IntelliSense
// when editing a .editorconfig file
Expand Down
60 changes: 60 additions & 0 deletions src/inlineComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export const INLINE_COMMENT_DIAGNOSTIC_CODE = 'inline-comment'
export const INLINE_COMMENT_DIAGNOSTIC_MESSAGE =
'Inline comments are not supported in EditorConfig. Move this comment to its own line.'

/**
* Matches an inline comment: any line that contains non-whitespace content
* followed by required whitespace and then an unquoted `#` or `;`.
* Lines where `#` or `;` is the first non-whitespace character are proper
* standalone comments and are excluded before this regex is applied.
*
* This mirrors the `inlineComment` rule in syntaxes/editorconfig.tmLanguage.json
* (`invalid.illegal.inline-comment.editorconfig`). Unfortunately VS Code's
* extension API does not expose TextMate token scopes at runtime, so the
* detection must be re-implemented here in TypeScript.
*/
const INLINE_COMMENT_RE = /\S.*?([ \t]+)([#;].*)$/

export type InlineCommentMatch = {
indentation: string
commentStart: number
separatorStart: number
commentText: string
}

export function findInlineComment(text: string): InlineCommentMatch | undefined {
const trimmed = text.trimStart()
if (
trimmed.length === 0 ||
trimmed.startsWith('#') ||
trimmed.startsWith(';')
) {
return
}

const match = INLINE_COMMENT_RE.exec(text)
if (!match) {
return
}

const commentText = match[2]
const commentStart = text.length - commentText.length
const separatorStart = commentStart - match[1].length
const indentation = text.match(/^\s*/)?.[0] ?? ''

return {
indentation,
commentStart,
separatorStart,
commentText,
}
}

export function moveInlineCommentToOwnLine(text: string, eol: string) {
const inlineComment = findInlineComment(text)
if (!inlineComment) {
return
}

return `${text.slice(0, inlineComment.separatorStart)}${eol}${inlineComment.indentation}${inlineComment.commentText}`
}
25 changes: 25 additions & 0 deletions src/test/inlineComments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as assert from 'assert'
import {
findInlineComment,
moveInlineCommentToOwnLine,
} from '../inlineComments'

suite('inline comment helpers', () => {
test('ignores standalone comments', () => {
assert.strictEqual(findInlineComment(' # already valid comment'), undefined)
})

test('moves property inline comments onto a new line', () => {
assert.strictEqual(
moveInlineCommentToOwnLine(' indent_style = space # required', '\n'),
' indent_style = space\n # required',
)
})

test('moves section inline comments onto a new line', () => {
assert.strictEqual(
moveInlineCommentToOwnLine('[*.md] ; note', '\n'),
'[*.md]\n; note',
)
})
})
51 changes: 50 additions & 1 deletion src/test/suite/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import * as assert from 'assert'
import * as os from 'os'
import { Position, window, workspace, WorkspaceEdit } from 'vscode'
import {
commands,
languages,
Position,
window,
workspace,
WorkspaceEdit,
} from 'vscode'
import { getFixturePath, getOptionsForFixture, wait } from '../testUtils'
import { MOVE_INLINE_COMMENT_TITLE } from '../../EditorConfigCodeActionProvider'

import * as utils from 'vscode-test-utils'

Expand Down Expand Up @@ -360,6 +368,47 @@ suite('EditorConfig extension', function () {
`document encoding is ${document.encoding} instead of iso88591`,
)
})

test('inline comment quick fix moves the comment to its own line', async () => {
const doc = await workspace.openTextDocument({
language: 'editorconfig',
content: 'indent_style = space # required',
})
await window.showTextDocument(doc)
await wait(200)

const diagnostics = languages.getDiagnostics(doc.uri)
assert.strictEqual(diagnostics.length, 1, 'expected one inline comment warning')

const actions = await commands.executeCommand<any[]>(
'vscode.executeCodeActionProvider',
doc.uri,
diagnostics[0].range,
)
assert.ok(actions, 'expected code actions')

const quickFix = actions.find(action => action.title === MOVE_INLINE_COMMENT_TITLE)
assert.ok(quickFix, 'expected inline comment quick fix')
assert.ok(quickFix.edit, 'expected quick fix edit')

assert.strictEqual(
await workspace.applyEdit(quickFix.edit),
true,
'editor fails to apply inline comment quick fix',
)
await wait(50)

assert.strictEqual(
doc.getText(),
'indent_style = space\n# required',
'inline comment quick fix did not move the comment to a new line',
)
assert.strictEqual(
languages.getDiagnostics(doc.uri).length,
0,
'inline comment warning should clear after applying the quick fix',
)
})
})

function withSetting(
Expand Down
10 changes: 10 additions & 0 deletions syntaxes/editorconfig.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@
}
]
},
"inlineComment": {
"name": "invalid.illegal.inline-comment.editorconfig",
"match": "\\s*[#;].*$"
},
"section": {
"name": "meta.section.editorconfig",
"begin": "^\\s*(?=\\[.*?\\])",
Expand All @@ -286,6 +290,9 @@
},
{
"include": "#rule"
},
{
"include": "#inlineComment"
}
]
},
Expand Down Expand Up @@ -374,6 +381,9 @@
},
{
"include": "#string"
},
{
"include": "#inlineComment"
}
]
}
Expand Down