Skip to content

Commit

Permalink
feat: add scaffold completion feature (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
yaegassy committed May 31, 2022
1 parent 4f7b5e4 commit 965dc5b
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ For more information, please check this link.
- This feature is a proprietary implementation of `coc-intelephense`. This feature will be removed when the dedicated feature is added in the upstream's `vscode-intelephense` or `intelephense` language server.
- I made it an ignore comment like `phpstan`, Please refer to this page for usage. <https://phpstan.org/user-guide/ignoring-errors#ignoring-in-code-using-phpdocs>
- `intelephense.client.autoCloseDocCommentDoSuggest`: When `/**` is entered, `*/` is automatically inserted (`/**| */`). Then, continue, automatically triggers completion of PHPDoc comments, default: `true` | [DEMO](https://github.com/yaegassy/coc-intelephense/pull/24#issuecomment-1088219510)
- `intelephense.client.disableScaffoldCompletion`: Disable scaffold completion (client). Typing `class_scaffold`, `interface_scaffold`, `trait_scaffold` or `enum_scaffold` will output completion suggestions. This completion feature will only work on the first line of the file, default: `false` | [DEMO](https://github.com/yaegassy/coc-intelephense/pull/36#issue-1254138261)
- `intelephense.client.disableSnippetsCompletion`: Disable snippets completion only (client), default: `false`
- `intelephense.client.snippetsCompletionExclude`: Exclude specific prefix in snippet completion, e.g. `["class", "fun"]`, default: `[]`
- `intelephense.client.disableCodeLens`: Disable code lens only (client), default: `false`
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
"default": true,
"description": "When `/**` is entered, `*/` is automatically inserted (`/**| */`). Then, continue, automatically triggers completion of PHPDoc comments."
},
"intelephense.client.disableScaffoldCompletion": {
"type": "boolean",
"default": false,
"description": "Disable scaffold completion (client). Typing `class_scaffold`, `interface_scaffold`, `trait_scaffold` or `enum_scaffold` will output completion suggestions. This completion feature will only work on the first line of the file."
},
"intelephense.client.disableSnippetsCompletion": {
"type": "boolean",
"default": false,
Expand Down
179 changes: 179 additions & 0 deletions src/completions/scaffold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
CancellationToken,
CompletionContext,
CompletionItem,
CompletionItemKind,
CompletionItemProvider,
CompletionList,
ExtensionContext,
InsertTextFormat,
languages,
Position,
TextDocument,
Uri,
workspace,
} from 'coc.nvim';
import fs from 'fs';
import path from 'path';

type ComposerJsonContentType = {
autoload: {
['psr-4']: {
[key: string]: string;
};
};
'autoload-dev': {
['psr-4']: {
[key: string]: string;
};
};
};

type NamespaceType = { [key: string]: string };

type CompletionDataEntryType = {
originalInsertText: string;
};

export function activate(context: ExtensionContext) {
if (!workspace.getConfiguration('intelephense').get('client.disableScaffoldCompletion', false)) {
context.subscriptions.push(
languages.registerCompletionItemProvider(
'intelephense-scaffold',
'intelephense',
['php'],
new ScaffoldCompletionProvider()
)
);
}
}

export class ScaffoldCompletionProvider implements CompletionItemProvider {
async getScaffoldCompletions(document: TextDocument) {
const scaffoldCompletionItems: CompletionItem[] = [];

const composerJsonPath = path.join(workspace.root, 'composer.json');
let composerJsonContent: ComposerJsonContentType | null = null;
try {
composerJsonContent = JSON.parse(fs.readFileSync(composerJsonPath, 'utf8'));
} catch (error) {
composerJsonContent = null;
}

const fileName = document.uri.split('/').slice(-1)[0].replace('.php', '');

let namespace = '';
if (composerJsonContent) {
const projectNamespaces = this.getProjectNamespacesFromComposerJson(composerJsonContent);
const workspaceUriPath = Uri.parse(workspace.root).toString();
const fileUriPath = document.uri;
const relativeFilePath = fileUriPath.replace(workspaceUriPath + '/', '');

const fileNamespace = this.getFileNamespace(projectNamespaces, relativeFilePath);
if (fileNamespace) namespace = fileNamespace;
}

const phpObjectTypes = ['class', 'interface', 'trait', 'enum'] as const;
phpObjectTypes.forEach((p) => {
const contents: string[] = [];
contents.push(`<?php\n`);
contents.push(`\n`);
contents.push(`declare(strict_types=1);\n`);
contents.push(`\n`);

if (namespace) {
contents.push(`namespace ${namespace};\n`);
contents.push(`\n`);
}

contents.push(`${p} ${fileName}\n`);
contents.push(`{\n`);
contents.push(`\t\${0:// ...code}\n`);
contents.push(`}\n`);

scaffoldCompletionItems.push({
label: `${p}_scaffold`,
kind: CompletionItemKind.Snippet,
detail: `${p} scaffold completion`,
documentation: { kind: 'markdown', value: '```php\n' + contents.join('') + '```' },
insertTextFormat: InsertTextFormat.Snippet,
data: <CompletionDataEntryType>{
originalInsertText: contents.join(''),
},
});
});

return scaffoldCompletionItems;
}

private getProjectNamespacesFromComposerJson(composerJsonContent: ComposerJsonContentType) {
const projectNamespaces: { [key: string]: string }[] = [];

if ('psr-4' in composerJsonContent.autoload) {
for (const [k, v] of Object.entries(composerJsonContent.autoload['psr-4'])) {
projectNamespaces.push({
[k]: v,
});
}
}

if ('psr-4' in composerJsonContent['autoload-dev']) {
for (const [k, v] of Object.entries(composerJsonContent['autoload-dev']['psr-4'])) {
projectNamespaces.push({
[k]: v,
});
}
}

return projectNamespaces;
}

private getFileNamespace(namespaces: NamespaceType[], relativeFilePath: string) {
const fileName = relativeFilePath.split('/').slice(-1)[0];

for (const namespace of namespaces) {
for (const k of Object.keys(namespace)) {
if (relativeFilePath.startsWith(namespace[k])) {
const fileNamespace = relativeFilePath
.replace(namespace[k], k)
.replace(/\//g, '\\')
.replace(fileName, '')
.replace(/\\$/, '');

return fileNamespace;
}
}
}
}

async provideCompletionItems(
document: TextDocument,
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 completionItems: CompletionItem[] = [];
// skip except for the first line.
if (position.line !== 0) return completionItems;

const doc = workspace.getDocument(document.uri);
if (!doc) return [];

const completions = await this.getScaffoldCompletions(document);
completionItems.push(...completions);

return completionItems;
}

async resolveCompletionItem(item: CompletionItem): Promise<CompletionItem> {
if (item.kind === CompletionItemKind.Snippet) {
if (item.data) {
const dataEntry = item.data as CompletionDataEntryType;
item.insertText = dataEntry.originalInsertText;
}
}
return item;
}
}
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as pestCommandFeature from './commands/pest';
import * as phpunitCommandFeature from './commands/phpunit';
import * as symfonyConsoleCommandFeature from './commands/symfonyConsole';
import * as autoCloseDocCommentDoSugesstCompletionFeature from './completions/autoCloseDocCommentDoSugesst';
import * as scaffoldCompletionFeature from './completions/scaffold';
import * as snippetsCompletionFeature from './completions/snippets';
import * as pestCodeLensFeature from './lenses/pest';
import * as phpunitCodeLensFeature from './lenses/phpunit';
Expand Down Expand Up @@ -86,10 +87,10 @@ export async function activate(context: ExtensionContext): Promise<void> {

clientDisposable = languageClient.start();

// Add snippets completion by "client" side
snippetsCompletionFeature.activate(context);
// Add auto close doc comment do sugesst completion by "client" side
// Add completion by "client" side
autoCloseDocCommentDoSugesstCompletionFeature.activate(context);
scaffoldCompletionFeature.activate(context);
snippetsCompletionFeature.activate(context);

// Add commands by "client" side
composerCommandFeature.activate(context);
Expand Down

0 comments on commit 965dc5b

Please sign in to comment.