Skip to content
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

feat: Blade Directive Completion #10

Merged
merged 1 commit into from
Mar 19, 2022
Merged
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
80 changes: 80 additions & 0 deletions data/completion/blade-directive.json
Original file line number Diff line number Diff line change
@@ -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)"
}
5 changes: 5 additions & 0 deletions data/completion/livewire-directive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"@livewireStyles": "Livewire Styles directive",
"@livewireScripts": "Livewire Scripts directive",
"@livewire": "Livewire nesting components"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"snippets:helper": "curl -o data/snippets/helpers.json https://github.com/raw/onecentlin/laravel-blade-snippets-vscode/master/snippets/helpers.json",
"snippets:livewire": "curl -o data/snippets/livewire.json https://github.com/raw/onecentlin/laravel-blade-snippets-vscode/master/snippets/livewire.json",
"snippets:snippets": "curl -o data/snippets/snippets.json https://github.com/raw/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",
Expand Down
49 changes: 49 additions & 0 deletions scripts/snippetsToCompletionJson.js
Original file line number Diff line number Diff line change
@@ -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')
);
95 changes: 95 additions & 0 deletions src/completion/provider/bladeDirective.ts
Original file line number Diff line number Diff line change
@@ -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<CompletionItem[] | CompletionList> {
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;
}
}
41 changes: 39 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -119,10 +120,36 @@ export async function activate(context: ExtensionContext): Promise<void> {
const isEnableCompletion = extConfig.get<boolean>('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
}
Expand All @@ -136,6 +163,16 @@ export async function activate(context: ExtensionContext): Promise<void> {
new BladeSnippetsCompletionProvider(context, outputChannel)
)
);

context.subscriptions.push(
languages.registerCompletionItemProvider(
'blade-directive',
'blade',
['blade'],
new BladeDirectiveCompletionProvider(context),
['@']
)
);
}

//
Expand Down