diff --git a/README.md b/README.md index e9ec70f..4b236be 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,22 @@ You can also run the installation command manually. - [Not implement] `ansible.ansible-playbook.run`: Run playbook via `ansible-playbook` - [Not implement] `ansible.ansible-navigator.run`: Run playbook via `ansible-navigator run` +## Code Actions + +**Example key mapping (Code Action related)**: + +```vim +nmap ga (coc-codeaction-line) +``` + +**Usage**: + +In the line with diagnostic message, enter the mapped key (e.g. `ga`) and you will see a list of code actions that can be performed. + +**Actions**: + +- `Ignoring rules for current line (# noqa [ruleId])` + ## Thanks - [ansible/ansible-language-server](https://github.com/ansible/ansible-language-server) diff --git a/src/action.ts b/src/action.ts new file mode 100644 index 0000000..6dab4c9 --- /dev/null +++ b/src/action.ts @@ -0,0 +1,100 @@ +import { + CodeAction, + CodeActionContext, + CodeActionProvider, + languages, + OutputChannel, + Range, + TextDocument, + TextEdit, + workspace, +} from 'coc.nvim'; + +export class AnsibleCodeActionProvider implements CodeActionProvider { + private readonly source = 'ansible'; + private diagnosticCollection = languages.createDiagnosticCollection(this.source); + private outputChannel: OutputChannel; + + constructor(outputChannel: OutputChannel) { + this.outputChannel = outputChannel; + } + + public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext) { + const doc = workspace.getDocument(document.uri); + const wholeRange = Range.create(0, 0, doc.lineCount, 0); + let whole = false; + if ( + range.start.line === wholeRange.start.line && + range.start.character === wholeRange.start.character && + range.end.line === wholeRange.end.line && + range.end.character === wholeRange.end.character + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + whole = true; + } + const codeActions: CodeAction[] = []; + + /** Ignoring rules for current line (# noqa [ruleId] */ + if (this.lineRange(range) && context.diagnostics.length > 0) { + const line = doc.getline(range.start.line); + this.outputChannel.append(line); + if (line && line.length) { + let existsAnsibleDiagnostics = false; + const ruleIds: string[] = []; + context.diagnostics.forEach((d) => { + if (d.source === 'Ansible') { + existsAnsibleDiagnostics = true; + const ruleId = this.parseRuleId(d.message); + if (ruleId) ruleIds.push(ruleId); + } + }); + + if (existsAnsibleDiagnostics) { + ruleIds.forEach((r) => { + let newText = ''; + if (line.match(/# noqa/)) { + newText = `${line} ${r}${range.start.line + 1 === range.end.line ? '\n' : ''}`; + } else { + newText = `${line} # noqa ${r}${range.start.line + 1 === range.end.line ? '\n' : ''}`; + } + + const edit = TextEdit.replace(range, newText); + + codeActions.push({ + title: `Ignoring rules for current line (# noqa ${r})`, + edit: { + changes: { + [doc.uri]: [edit], + }, + }, + }); + }); + } + } + } + + return codeActions; + } + + private lineRange(r: Range): boolean { + return ( + (r.start.line + 1 === r.end.line && r.start.character === 0 && r.end.character === 0) || + (r.start.line === r.end.line && r.start.character === 0) + ); + } + + public parseRuleId(s: string): string | undefined { + let r: string | undefined; + const l = s.split('\n'); + + const p = /^(?:\[(?.*)\].*)$/; + const m = l[0].match(p); + if (m) { + if (m.groups?.ruleId) { + r = m.groups.ruleId; + } + } + + return r; + } +} diff --git a/src/index.ts b/src/index.ts index 5f6eb8e..5296458 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { ExtensionContext, LanguageClient, LanguageClientOptions, + languages, ServerOptions, Thenable, TransportKind, @@ -16,6 +17,7 @@ import fs from 'fs'; import path from 'path'; import { AnsiblePlaybookRunProvider } from './features/runner'; +import { AnsibleCodeActionProvider } from './action'; import { installLsRequirementsTools } from './installer'; import { existsCmdWithHelpOpt, @@ -172,11 +174,13 @@ export async function activate(context: ExtensionContext): Promise { }, }; + const documentSelector = [ + { scheme: 'file', language: 'ansible' }, + { scheme: 'file', language: 'yaml.ansible' }, + ]; + const clientOptions: LanguageClientOptions = { - documentSelector: [ - { scheme: 'file', language: 'ansible' }, - { scheme: 'file', language: 'yaml.ansible' }, - ], + documentSelector, middleware: { workspace: { configuration, @@ -185,8 +189,10 @@ export async function activate(context: ExtensionContext): Promise { }; client = new LanguageClient('ansibleServer', 'Ansible Server', serverOptions, clientOptions); - client.start(); + + const actionProvider = new AnsibleCodeActionProvider(outputChannel); + context.subscriptions.push(languages.registerCodeActionProvider(documentSelector, actionProvider, 'ansible')); } export function deactivate(): Thenable | undefined {