From 2ba0cc3f44ee3bbc677c27a181b895b3388aaf07 Mon Sep 17 00:00:00 2001 From: yaegassy Date: Sat, 19 Mar 2022 11:14:42 +0900 Subject: [PATCH] feat: Blade Directive Completion --- data/completion/blade-directive.json | 80 +++++++++++++++++++ data/completion/livewire-directive.json | 5 ++ package.json | 3 +- scripts/snippetsToCompletionJson.js | 49 ++++++++++++ src/completion/provider/bladeDirective.ts | 95 +++++++++++++++++++++++ src/index.ts | 41 +++++++++- 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 data/completion/blade-directive.json create mode 100644 data/completion/livewire-directive.json create mode 100644 scripts/snippetsToCompletionJson.js create mode 100644 src/completion/provider/bladeDirective.ts diff --git a/data/completion/blade-directive.json b/data/completion/blade-directive.json new file mode 100644 index 0000000..926f58f --- /dev/null +++ b/data/completion/blade-directive.json @@ -0,0 +1,80 @@ +{ + "@extends": "extends view layout", + "@yield": "yield content section", + "@section": "content section show", + "@endsection": "content section", + "@show": "content section show", + "@include": "include view", + "@if": "$loop->last", + "@endif": "$loop->last", + "@else": "@hasSection condition", + "@hasSection": "@hasSection condition", + "@unless": "@unless block", + "@endunless": "@unless block", + "@for": "@for block", + "@endfor": "@for block", + "@foreach": "@foreach block", + "@endforeach": "@foreach block", + "@forelse": "@forelse block", + "@empty": "empty", + "@endforelse": "@forelse block", + "@while": "@while block", + "@endwhile": "@while block", + "@each": "@each loop", + "@verbatim": "displaying JavaScript variables in a large portion of your template", + "@endverbatim": "displaying JavaScript variables in a large portion of your template", + "@push": "@push stack", + "@endpush": "@push stack", + "@stack": "@stack", + "@inject": "@inject Service", + "@can": "display a portion of the page only if the user is authorized to perform a given action.", + "@endcan": "display a portion of the page only if the user is authorized to perform a given action.", + "@elsecan": "display a portion of the page only if the user is authorized to perform a given action.", + "@canany": "display a portion of the page only if the user is authorized to perform a given action.", + "@endcanany": "display a portion of the page only if the user is authorized to perform a given action.", + "@elsecanany": "display a portion of the page only if the user is authorized to perform a given action.", + "@cannot": "display a portion of the page only if the user is authorized to perform a given action.", + "@endcannot": "display a portion of the page only if the user is authorized to perform a given action.", + "@elsecannot": "display a portion of the page only if the user is authorized to perform a given action.", + "@php": "@php block code in view", + "@endphp": "@php block code in view", + "@includeIf": "include a view that may or may not be present, you should use the @includeIf directive", + "@component": "component", + "@endcomponent": "component", + "@slot": "slot", + "@endslot": "slot", + "@isset": "isset", + "@endisset": "isset", + "@endempty": "empty", + "@error": "error", + "@enderror": "error", + "@includeWhen": "includeWhen", + "@auth": "auth", + "@endauth": "auth", + "@guest": "guest", + "@endguest": "guest", + "@switch": "switch", + "@case": "switch", + "@break": "switch", + "@default": "switch", + "@endswitch": "switch", + "@includeFirst": "includeFirst", + "@csrf": "form csrf field", + "@method": "form method field", + "@dump": "dump", + "@lang": "lang", + "@includeUnless": "includeUnless", + "@props": "Blade component data properties", + "@env": "env", + "@endenv": "env", + "@production": "production", + "@endproduction": "production", + "@once": "define a portion of template that will only be evaluated once per rendering cycle", + "@endonce": "define a portion of template that will only be evaluated once per rendering cycle", + "@aware": "Accessing data from a parent component (Laravel 8.64)", + "@js": "This directive is useful to properly escape JSON within HTML quotes", + "@class": "conditionally compiles a CSS class string. (Laravel 8.51)", + "@checked": "This directive will echo checked if the provided condition evaluates to true (Laravel 9.x)", + "@selected": "The @selected directive may be used to indicate if a given select option should be \"selected\" (Laravel 9.x)", + "@disabled": "The @disabled directive may be used to indicate if a given element should be \"disabled\" (Laravel 9.x)" +} diff --git a/data/completion/livewire-directive.json b/data/completion/livewire-directive.json new file mode 100644 index 0000000..0b407af --- /dev/null +++ b/data/completion/livewire-directive.json @@ -0,0 +1,5 @@ +{ + "@livewireStyles": "Livewire Styles directive", + "@livewireScripts": "Livewire Scripts directive", + "@livewire": "Livewire nesting components" +} diff --git a/package.json b/package.json index 475a109..e555e48 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "snippets:helper": "curl -o data/snippets/helpers.json https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/master/snippets/helpers.json", "snippets:livewire": "curl -o data/snippets/livewire.json https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/master/snippets/livewire.json", "snippets:snippets": "curl -o data/snippets/snippets.json https://raw.githubusercontent.com/onecentlin/laravel-blade-snippets-vscode/master/snippets/snippets.json", - "data:format": "prettier --write data/snippets/*.json", + "generate": "node scripts/snippetsToCompletionJson.js && yarn data:format", + "data:format": "prettier --write data/snippets/*.json && prettier --write data/completion/*.json", "lint": "eslint src --ext ts", "clean": "rimraf lib", "watch": "node esbuild.js --watch", diff --git a/scripts/snippetsToCompletionJson.js b/scripts/snippetsToCompletionJson.js new file mode 100644 index 0000000..3c6ff9d --- /dev/null +++ b/scripts/snippetsToCompletionJson.js @@ -0,0 +1,49 @@ +const fs = require('fs'); +const path = require('path'); + +const generate = (filename, outputPath) => { + fs.readFile(filename, 'utf8', (err, data) => { + if (err) { + console.error(err); + return; + } + + jsonData = JSON.parse(data); + + const output = {}; + for (const key of Object.keys(jsonData)) { + let body = ''; + if (typeof jsonData[key]['body'] === 'string' || jsonData[key]['body'] instanceof String) { + body = jsonData[key]['body']; + } else if (Array.isArray(jsonData[key]['body'])) { + body = jsonData[key]['body'].join(''); + } + + if (body) { + matches = body.match(/@.[a-zA-Z_0-9]*/g); + if (matches) { + matches.forEach((v) => { + if (v !== '@{') { + output[v] = jsonData[key]['description']; + } + }); + } + } + } + + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + } + + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); + }); +}; + +generate( + path.join(path.dirname(__dirname), 'data', 'snippets', 'snippets.json'), + path.join(path.dirname(__dirname), 'data', 'completion', 'blade-directive.json') +); +generate( + path.join(path.dirname(__dirname), 'data', 'snippets', 'livewire.json'), + path.join(path.dirname(__dirname), 'data', 'completion', 'livewire-directive.json') +); diff --git a/src/completion/provider/bladeDirective.ts b/src/completion/provider/bladeDirective.ts new file mode 100644 index 0000000..a1be4a2 --- /dev/null +++ b/src/completion/provider/bladeDirective.ts @@ -0,0 +1,95 @@ +import { + CancellationToken, + CompletionContext, + CompletionItem, + CompletionItemKind, + CompletionItemProvider, + CompletionList, + ExtensionContext, + LinesTextDocument, + Position, + workspace, +} from 'coc.nvim'; + +import path from 'path'; +import fs from 'fs'; + +type CompletionJsonType = { + [key: string]: string; +}; + +export class BladeDirectiveCompletionProvider implements CompletionItemProvider { + private _context: ExtensionContext; + private directiveJsonPaths: string[]; + + constructor(context: ExtensionContext) { + this._context = context; + this.directiveJsonPaths = [ + path.join(this._context.extensionPath, 'data', 'completion', 'blade-directive.json'), + path.join(this._context.extensionPath, 'data', 'completion', 'livewire-directive.json'), + ]; + } + + async getCompletionItems(completionDataPath: string) { + const completionList: CompletionItem[] = []; + if (fs.existsSync(completionDataPath)) { + const completionJsonText = fs.readFileSync(completionDataPath, 'utf8'); + const completionJson: CompletionJsonType = JSON.parse(completionJsonText); + if (completionJson) { + Object.keys(completionJson).map((key) => { + const docDataPath = path.join( + this._context.extensionPath, + 'data', + 'documantation', + 'blade', + key.replace('@', '') + '.md' + ); + + let documentationText: string | undefined; + try { + documentationText = fs.readFileSync(docDataPath, 'utf8'); + } catch (e) { + // noop + documentationText = undefined; + } + + completionList.push({ + label: key, + kind: CompletionItemKind.Text, + insertText: key.replace('@', ''), + detail: completionJson[key], + documentation: documentationText, + }); + }); + } + } + + return completionList; + } + + async provideCompletionItems( + //document: TextDocument, + document: LinesTextDocument, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + token: CancellationToken, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: CompletionContext + ): Promise { + const doc = workspace.getDocument(document.uri); + if (!doc) return []; + const wordRange = doc.getWordRangeAtPosition(Position.create(position.line, position.character - 1), '@'); + if (!wordRange) return []; + const text = document.getText(wordRange) || ''; + if (!text) return []; + + if (!text.startsWith('@')) return []; + + const completionItemList: CompletionItem[] = []; + this.directiveJsonPaths.forEach((v) => { + this.getCompletionItems(v).then((vv) => completionItemList.push(...vv)); + }); + return completionItemList; + } +} diff --git a/src/index.ts b/src/index.ts index 5bb97d0..aab11c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import path from 'path'; import { BladeHoverProvider } from './hover/hover'; import { BladeSnippetsCompletionProvider } from './completion/provider/bladeSnippets'; +import { BladeDirectiveCompletionProvider } from './completion/provider/bladeDirective'; import { BladelinterLintEngine } from './lint'; import BladeFormattingEditProvider, { doFormat, fullDocumentRange } from './format'; import BladeDefinitionProvider from './definition'; @@ -119,10 +120,36 @@ export async function activate(context: ExtensionContext): Promise { const isEnableCompletion = extConfig.get('completion.enable', true); if (isEnableCompletion) { const { document } = await workspace.getCurrentState(); + const indentexpr = await (await workspace.nvim.buffer).getOption('indentexpr'); if (document.languageId === 'blade') { try { - await workspace.nvim.command('setlocal iskeyword+=:'); - await workspace.nvim.command('setlocal iskeyword+=-'); + workspace.registerAutocmd({ + event: 'FileType', + pattern: 'blade', + request: true, + callback: async () => { + await workspace.nvim.command('setlocal iskeyword+=:'); + await workspace.nvim.command('setlocal iskeyword+=-'); + }, + }); + + workspace.registerAutocmd({ + event: 'InsertEnter', + pattern: '*.blade.php', + request: true, + callback: async () => { + await workspace.nvim.command('setlocal indentexpr='); + }, + }); + + workspace.registerAutocmd({ + event: 'InsertLeave', + pattern: '*.blade.php', + request: true, + callback: async () => { + await workspace.nvim.command(`setlocal indentexpr=${indentexpr}`); + }, + }); } catch { // noop } @@ -136,6 +163,16 @@ export async function activate(context: ExtensionContext): Promise { new BladeSnippetsCompletionProvider(context, outputChannel) ) ); + + context.subscriptions.push( + languages.registerCompletionItemProvider( + 'blade-directive', + 'blade', + ['blade'], + new BladeDirectiveCompletionProvider(context), + ['@'] + ) + ); } //