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: add "intelephense.fixNamespace" Command #35

Merged
merged 2 commits into from
May 31, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ For more information, please check this link.
- `intelephense.pest.fileTest`: Run Pest for current file
- `intelephense.pest.singleTest`: Run Pest for single (nearest) test
- `intelephense.fixClassName`: Fix class name based on file name | [DEMO](https://github.com/yaegassy/coc-intelephense/pull/34#issue-1253763775)
- `intelephense.fixNamespace`: Fix namespace based on the composer configuration | [DEMO](https://github.com/yaegassy/coc-intelephense/pull/35#issue-1253784395)

**Example of Vim command and key mapping**:

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,10 @@
"command": "intelephense.fixClassName",
"title": "Fix class name based on file name",
"category": "Intelephense"
},
{
"command": "intelephense.fixNamespace",
"title": "Fix namespace based on the composer configuration"
}
]
},
Expand Down
126 changes: 126 additions & 0 deletions src/commands/fixNamespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { commands, ExtensionContext, window, workspace, Uri, TextEdit, Range, Position } from 'coc.nvim';
import * as fixNamespaceParser from '../parsers/fixNamespace';

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 };

export function activate(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand('intelephense.fixNamespace', runFixNamespace()));
}

export function runFixNamespace() {
return async () => {
const { document } = await workspace.getCurrentState();

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;
}

if (!composerJsonContent) {
window.showErrorMessage(`composer.json could not be loaded`);
return;
}

const projectNamespaces = getProjectNamespacesFromComposerJson(composerJsonContent);
const workspaceUriPath = Uri.parse(workspace.root).toString();
const fileUriPath = document.uri;
const relativeFilePath = fileUriPath.replace(workspaceUriPath + '/', '');
const newNamespace = getFileNamespace(projectNamespaces, relativeFilePath);

// ---- edit namespace ----

const ast = fixNamespaceParser.getAst(document.getText());
if (!ast) return;

const currentFileNsNode = fixNamespaceParser.getNamespaceNode(ast.children);
if (!currentFileNsNode) {
window.showErrorMessage('namespace not found in target file');
return;
}

const currentFileNsLoc = fixNamespaceParser.getNamespaceLocation(currentFileNsNode);
if (!currentFileNsLoc) {
window.showErrorMessage('Failed to parse namespace in target file');
return;
}

const doc = await workspace.openTextDocument(Uri.parse(document.uri).fsPath);

// **WARNING**:
// The end of the namespace location is the end of all nodes in the namespace.
// Not the end of the line declared in namespace
const declarationContent = doc.getline(currentFileNsLoc.start.line - 1);
const declarationColumn = declarationContent.length;

const edits = [
TextEdit.replace(
Range.create(
Position.create(currentFileNsLoc.start.line - 1, currentFileNsLoc.start.column),
Position.create(currentFileNsLoc.start.line - 1, declarationColumn)
),
`namespace ${newNamespace};`
),
];

await doc.applyEdits(edits);
};
}

function 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;
}

function 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;
}
}
}
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ import {
workspace,
} from 'coc.nvim';
import fs from 'fs';
import * as ignoreCommentCodeActionFeature from './actions/ignoreComment';
import * as getterSetterCodeActionFeature from './actions/getterSetter';
import * as ignoreCommentCodeActionFeature from './actions/ignoreComment';
import * as openPHPNetCodeActionFeature from './actions/openPHPNet';
import * as composerCommandFeature from './commands/composer';
import * as fixClassNameCommandFeature from './commands/fixClassName';
import * as fixNamespaceCommandFeature from './commands/fixNamespace';
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 snippetsCompletionFeature from './completions/snippets';
import * as pestCodeLensFeature from './lenses/pest';
import * as phpunitCodeLensFeature from './lenses/phpunit';
import * as fixClassNameCommandFeature from './commands/fixClassName';

const PHP_LANGUAGE_ID = 'php';
const INDEXING_STARTED_NOTIFICATION = new NotificationType('indexingStarted');
Expand Down Expand Up @@ -96,6 +97,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
phpunitCommandFeature.activate(context);
pestCommandFeature.activate(context);
fixClassNameCommandFeature.activate(context);
fixNamespaceCommandFeature.activate(context);

// Add code lens by "client" side
phpunitCodeLensFeature.activate(context);
Expand Down
61 changes: 61 additions & 0 deletions src/parsers/fixNamespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Class, Engine, Namespace, Node } from 'php-parser';

const parserEngine = new Engine({
parser: {
extractDoc: true,
php7: true,
locations: true,
suppressErrors: true,
},
ast: {
all_tokens: true,
withPositions: true,
},
});

export function getAst(code: string) {
try {
return parserEngine.parseEval(stripPHPTag(code));
} catch (e) {
return undefined;
}

function stripPHPTag(code: string): string {
return code.replace('<?php', '').replace('?>', '');
}
}

export function getClassesNodes(nodes: Node[]): Class[] {
const classNodes: Class[] = [];

for (const node of nodes) {
if (node.kind === 'namespace') {
const namespaceNode = node as Namespace;
return getClassesNodes(namespaceNode.children);
}

if (node.kind === 'class') {
const classNode = node as Class;
classNodes.push(classNode);
}
}

return classNodes;
}

export function getNamespaceNode(nodes: Node[]): Namespace | undefined {
for (const node of nodes) {
if (node.kind === 'namespace') {
const namespaceNode = node as Namespace;
return namespaceNode;
}
}
}

export function getNamespaceLocation(namespace: Namespace) {
if (namespace.kind !== 'namespace') return;

if (namespace.loc) {
return namespace.loc;
}
}