Skip to content
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ This extension contributes the following settings:
"phpNamespaceRefactor.additionalExtensions": [
"php"
],
"phpNamespaceRefactor.rename": true
}
```

Expand Down Expand Up @@ -73,6 +74,14 @@ This extension contributes the following settings:

- Default: "php".

**phpNamespaceRefactor.rename**

- Can be triggered by pressing F2 or the preferred rename shortcut.
- The feature can be enabled or disabled in the settings.

- Default: true.


## Release notes

See [./CHANGELOG.md](./CHANGELOG.md)
Expand Down
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "phpNamespaceRefactor.rename",
"title": "Rename Namespace",
"category": "PHP Namespace Refactor"
}
],
"keybindings": [
{
"command": "phpNamespaceRefactor.rename",
"key": "f2",
"when": "editorTextFocus && resourceExtname == .php"
}
],
"configuration": {
"languages": [
{
Expand Down Expand Up @@ -84,6 +98,11 @@
"php"
],
"description": "List of additional file extensions to consider for namespace refactoring."
},
"phpNamespaceRefactor.rename": {
"type": "boolean",
"default": true,
"description": "Enable or disable the Rename Namespace or Class command."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { FileRenameEvent, Uri, workspace, WorkspaceEdit } from 'vscode';
import { inject, injectable } from 'tsyringe';
import { FileRenameEvent } from 'vscode';
import { FileRenameFeature } from '@app/features/FileRenameFeature';

interface Props {
oldUri: Uri
newUri: Uri
}

@injectable()
export class FileRenameHandler {
constructor(
@inject(FileRenameFeature) private fileRenameFeature: FileRenameFeature,
) {}

public static create({ oldUri, newUri }: Props): void {
const edit = new WorkspaceEdit();
edit.renameFile(oldUri, newUri);
workspace.applyEdit(edit);
}

public async handle(event: FileRenameEvent) {
await this.fileRenameFeature.execute(event.files);
}
Expand Down
31 changes: 31 additions & 0 deletions src/app/commands/RenameHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { inject, injectable } from 'tsyringe';
import { PHP_EXTENSION } from '@infra/utils/constants';
import { RenameFeature } from '@app/features/RenameFeature';
import { TextEditor } from 'vscode';

interface Props {
activeEditor?: TextEditor;
}

@injectable()
export class RenameHandler {
constructor(
@inject(RenameFeature) private readonly renameFeature: RenameFeature
) {}

public async handle({ activeEditor }: Props) {
if (!activeEditor) {
return;
}

const document = activeEditor.document;
if (!document.fileName.endsWith(PHP_EXTENSION)) {
return;
}

await this.renameFeature.execute({
document,
position: activeEditor.selection.active,
});
}
}
40 changes: 40 additions & 0 deletions src/app/features/RenameFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { inject, injectable } from 'tsyringe';
import { Position, TextDocument, Uri, window } from 'vscode';
import { ExtractNameFromCursor } from '@app/services/rename/ExctratNameFromCursor';
import { FileRenameResolver } from '@app/services/rename/FileRenameResolver';
import { RenameValidator } from '@app/services/rename/RenameValidator';

interface Props {
document: TextDocument
position: Position
}

@injectable()
export class RenameFeature {
constructor(
@inject(ExtractNameFromCursor) private extractNameFromCursor: ExtractNameFromCursor,
@inject(RenameValidator) private renameValidator: RenameValidator,
@inject(FileRenameResolver) private fileRenameResolver: FileRenameResolver,
) {
}

public async execute({ document, position }: Props): Promise<void> {
const value = await this.extractNameFromCursor.execute({ document, position });
if (!value) {
return;
}

const newName = await window.showInputBox({
value,
title: '',
prompt: '',
validateInput: (value: string) => this.renameValidator.validate(value)
});

if (!newName || newName.trim() === '') {
return;
}

this.fileRenameResolver.execute({ document, position, newName });
}
}
35 changes: 35 additions & 0 deletions src/app/services/rename/ExctratNameFromCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Position, TextDocument } from 'vscode';
import { injectable } from 'tsyringe';
import { PHP_CLASS_DECLARATION_REGEX } from '@app/services/update/ClassNameUpdater';

interface Props {
document: TextDocument
position: Position
}

const NAMESPACE_REGEX = /^\s*namespace\s+([\w\\]+);/;

@injectable()
export class ExtractNameFromCursor {
public async execute({ document, position }: Props): Promise<string|null> {
const text = document.getText();
const lines = text.split('\n');

const currentLine = lines[position.line] ?? null;
if (null === currentLine) {
return null;
}

const namespaceMatch = currentLine.match(NAMESPACE_REGEX);
if (namespaceMatch) {
return namespaceMatch[1] ?? null;
}

const classMatch = currentLine.match(PHP_CLASS_DECLARATION_REGEX);
if (classMatch) {
return classMatch[1] ?? null;
}

return null;
}
}
54 changes: 54 additions & 0 deletions src/app/services/rename/FileRenameResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { inject, injectable } from 'tsyringe';
import { Position, TextDocument, Uri, window } from 'vscode';
import { FileRenameHandler } from '@app/commands/FileRenameHandler';
import { RenameTypeDetector } from './RenameTypeDetector';
import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver';

interface Props {
document: TextDocument
position: Position
newName: string
}

@injectable()
export class FileRenameResolver {
constructor(
@inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver,
@inject(RenameTypeDetector) private renameTypeDetector: RenameTypeDetector,
) {
}

public async execute({ document, position, newName }: Props): Promise<void> {
if (!newName.length) {
throw new Error('New name cannot be empty');
}

const type = this.renameTypeDetector.execute({ document, position });

const currentUri = document.uri;
const oldPath = currentUri.fsPath;
const fileName = this.workspacePathResolver.extractClassNameFromPath(oldPath);

if (type === 'namespace') {
try {
const newDirectoryPath = await this.workspacePathResolver.getDirectoryFromNamespace(newName);

FileRenameHandler.create({
oldUri: currentUri,
newUri: Uri.file(`${newDirectoryPath}/${fileName}.php`),
});
return;
} catch (error) {
window.showErrorMessage(`Error renaming namespace: ${error}`);
}
}

const directory = oldPath.substring(0, oldPath.lastIndexOf('/'));
const extension = oldPath.substring(oldPath.lastIndexOf('.'));

FileRenameHandler.create({
oldUri: currentUri,
newUri: Uri.file(`${directory}/${fileName}${extension}`),
});
}
}
28 changes: 28 additions & 0 deletions src/app/services/rename/RenameTypeDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Position, TextDocument } from 'vscode';
import { injectable } from 'tsyringe';

interface Props {
document: TextDocument
position: Position
}

type TypeRename = 'namespace' | 'class';

@injectable()
export class RenameTypeDetector {
public execute({ document, position }: Props): TypeRename {
const text = document.getText();
const lines = text.split('\n');
const currentLine = lines[position.line] ?? '';

if (currentLine.match(/^\s*namespace\s+/)) {
return 'namespace';
}

if (currentLine.match(/^\s*(?:abstract\s+)?(?:final\s+)?(?:class|interface|trait)\s+/)) {
return 'class';
}

throw new Error('Type rename not identified');
}
}
34 changes: 34 additions & 0 deletions src/app/services/rename/RenameValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { injectable } from 'tsyringe';

const NAMESPACE_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(\\[A-Za-z_][A-Za-z0-9_]*)*$/;

@injectable()
export class RenameValidator {
public validate(value: string): string | null {
if (!value || value.trim() === '') {
return 'Name cannot be empty';
}

if (value.startsWith('\\')) {
return 'Cannot start with \\';
}

if (value.endsWith('\\')) {
return 'Cannot end with \\';
}

if (value.includes('\\\\')) {
return 'Cannot contain double \\\\';
}

if (!/^[A-Za-z]/.test(value)) {
return 'Must start with a letter';
}

if (!NAMESPACE_REGEX.test(value)) {
return 'Invalid format. Use: Foo\\Bar';
}

return null;
}
}
4 changes: 2 additions & 2 deletions src/app/services/update/ClassNameUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Range, Uri, workspace, WorkspaceEdit } from "vscode";
import { TextDocumentOpener } from "../TextDocumentOpener";
import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver';

const CLASS_REGEX = /^\s*(?:abstract\s+)?(?:final\s+)?(?:class|interface|trait)\s+(\w+)/m;
export const PHP_CLASS_DECLARATION_REGEX = /^\s*(?:abstract\s+)?(?:final\s+)?(?:class|interface|trait)\s+(\w+)/m;

interface Props {
newUri: Uri,
Expand All @@ -19,7 +19,7 @@ export class ClassNameUpdater {
public async execute({ newUri }: Props): Promise<void> {
const { document, text } = await this.textDocumentOpener.execute({ uri: newUri });

const match = CLASS_REGEX.exec(text);
const match = PHP_CLASS_DECLARATION_REGEX.exec(text);
if (!match) {
return;
}
Expand Down
11 changes: 7 additions & 4 deletions src/domain/workspace/ConfigurationLocator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { workspace, WorkspaceConfiguration } from 'vscode';
import { injectable } from "tsyringe";

export const Config = 'phpNamespaceRefactor';

export const ConfigKeys = {
AUTO_IMPORT_NAMESPACE: 'autoImportNamespace',
REMOVE_UNUSED_IMPORTS: 'removeUnusedImports',
IGNORED_DIRECTORIES: 'ignoredDirectories',
ADDITIONAL_EXTENSIONS: 'additionalExtensions',
RENAME: 'rename',
} as const;

export type Props<T> = {
Expand All @@ -15,11 +18,11 @@ export type Props<T> = {

@injectable()
export class ConfigurationLocator {
private config: WorkspaceConfiguration;
private config: WorkspaceConfiguration;

constructor() {
this.config = workspace.getConfiguration('phpNamespaceRefactor');
}
constructor() {
this.config = workspace.getConfiguration(Config);
}

public get<T>({ key, defaultValue }: Props<T>): T {
return this.config.get<T>(key, defaultValue as T);
Expand Down
Loading