Skip to content

Commit 7adcdf2

Browse files
authored
Merge pull request #46 from rejmann/feature/#45
feat: #45 - edit namespace or class name within the editor and also refactor in the project
2 parents 3323f9f + e7fae62 commit 7adcdf2

16 files changed

Lines changed: 346 additions & 33 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ This extension contributes the following settings:
4444
"phpNamespaceRefactor.additionalExtensions": [
4545
"php"
4646
],
47+
"phpNamespaceRefactor.rename": true
4748
}
4849
```
4950

@@ -73,6 +74,14 @@ This extension contributes the following settings:
7374

7475
- Default: "php".
7576

77+
**phpNamespaceRefactor.rename**
78+
79+
- Can be triggered by pressing F2 or the preferred rename shortcut.
80+
- The feature can be enabled or disabled in the settings.
81+
82+
- Default: true.
83+
84+
7685
## Release notes
7786

7887
See [./CHANGELOG.md](./CHANGELOG.md)

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@
3939
],
4040
"main": "./dist/extension.js",
4141
"contributes": {
42+
"commands": [
43+
{
44+
"command": "phpNamespaceRefactor.rename",
45+
"title": "Rename Namespace",
46+
"category": "PHP Namespace Refactor"
47+
}
48+
],
49+
"keybindings": [
50+
{
51+
"command": "phpNamespaceRefactor.rename",
52+
"key": "f2",
53+
"when": "editorTextFocus && resourceExtname == .php"
54+
}
55+
],
4256
"configuration": {
4357
"languages": [
4458
{
@@ -84,6 +98,11 @@
8498
"php"
8599
],
86100
"description": "List of additional file extensions to consider for namespace refactoring."
101+
},
102+
"phpNamespaceRefactor.rename": {
103+
"type": "boolean",
104+
"default": true,
105+
"description": "Enable or disable the Rename Namespace or Class command."
87106
}
88107
}
89108
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1+
import { FileRenameEvent, Uri, workspace, WorkspaceEdit } from 'vscode';
12
import { inject, injectable } from 'tsyringe';
2-
import { FileRenameEvent } from 'vscode';
33
import { FileRenameFeature } from '@app/features/FileRenameFeature';
44

5+
interface Props {
6+
oldUri: Uri
7+
newUri: Uri
8+
}
9+
510
@injectable()
611
export class FileRenameHandler {
712
constructor(
813
@inject(FileRenameFeature) private fileRenameFeature: FileRenameFeature,
914
) {}
1015

16+
public static create({ oldUri, newUri }: Props): void {
17+
const edit = new WorkspaceEdit();
18+
edit.renameFile(oldUri, newUri);
19+
workspace.applyEdit(edit);
20+
}
21+
1122
public async handle(event: FileRenameEvent) {
1223
await this.fileRenameFeature.execute(event.files);
1324
}

src/app/commands/RenameHandler.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { inject, injectable } from 'tsyringe';
2+
import { PHP_EXTENSION } from '@infra/utils/constants';
3+
import { RenameFeature } from '@app/features/RenameFeature';
4+
import { TextEditor } from 'vscode';
5+
6+
interface Props {
7+
activeEditor?: TextEditor;
8+
}
9+
10+
@injectable()
11+
export class RenameHandler {
12+
constructor(
13+
@inject(RenameFeature) private readonly renameFeature: RenameFeature
14+
) {}
15+
16+
public async handle({ activeEditor }: Props) {
17+
if (!activeEditor) {
18+
return;
19+
}
20+
21+
const document = activeEditor.document;
22+
if (!document.fileName.endsWith(PHP_EXTENSION)) {
23+
return;
24+
}
25+
26+
await this.renameFeature.execute({
27+
document,
28+
position: activeEditor.selection.active,
29+
});
30+
}
31+
}

src/app/features/RenameFeature.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { inject, injectable } from 'tsyringe';
2+
import { Position, TextDocument, Uri, window } from 'vscode';
3+
import { ExtractNameFromCursor } from '@app/services/rename/ExctratNameFromCursor';
4+
import { FileRenameResolver } from '@app/services/rename/FileRenameResolver';
5+
import { RenameValidator } from '@app/services/rename/RenameValidator';
6+
7+
interface Props {
8+
document: TextDocument
9+
position: Position
10+
}
11+
12+
@injectable()
13+
export class RenameFeature {
14+
constructor(
15+
@inject(ExtractNameFromCursor) private extractNameFromCursor: ExtractNameFromCursor,
16+
@inject(RenameValidator) private renameValidator: RenameValidator,
17+
@inject(FileRenameResolver) private fileRenameResolver: FileRenameResolver,
18+
) {
19+
}
20+
21+
public async execute({ document, position }: Props): Promise<void> {
22+
const value = await this.extractNameFromCursor.execute({ document, position });
23+
if (!value) {
24+
return;
25+
}
26+
27+
const newName = await window.showInputBox({
28+
value,
29+
title: '',
30+
prompt: '',
31+
validateInput: (value: string) => this.renameValidator.validate(value)
32+
});
33+
34+
if (!newName || newName.trim() === '') {
35+
return;
36+
}
37+
38+
this.fileRenameResolver.execute({ document, position, newName });
39+
}
40+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Position, TextDocument } from 'vscode';
2+
import { injectable } from 'tsyringe';
3+
import { PHP_CLASS_DECLARATION_REGEX } from '@app/services/update/ClassNameUpdater';
4+
5+
interface Props {
6+
document: TextDocument
7+
position: Position
8+
}
9+
10+
const NAMESPACE_REGEX = /^\s*namespace\s+([\w\\]+);/;
11+
12+
@injectable()
13+
export class ExtractNameFromCursor {
14+
public async execute({ document, position }: Props): Promise<string|null> {
15+
const text = document.getText();
16+
const lines = text.split('\n');
17+
18+
const currentLine = lines[position.line] ?? null;
19+
if (null === currentLine) {
20+
return null;
21+
}
22+
23+
const namespaceMatch = currentLine.match(NAMESPACE_REGEX);
24+
if (namespaceMatch) {
25+
return namespaceMatch[1] ?? null;
26+
}
27+
28+
const classMatch = currentLine.match(PHP_CLASS_DECLARATION_REGEX);
29+
if (classMatch) {
30+
return classMatch[1] ?? null;
31+
}
32+
33+
return null;
34+
}
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { inject, injectable } from 'tsyringe';
2+
import { Position, TextDocument, Uri, window } from 'vscode';
3+
import { FileRenameHandler } from '@app/commands/FileRenameHandler';
4+
import { RenameTypeDetector } from './RenameTypeDetector';
5+
import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver';
6+
7+
interface Props {
8+
document: TextDocument
9+
position: Position
10+
newName: string
11+
}
12+
13+
@injectable()
14+
export class FileRenameResolver {
15+
constructor(
16+
@inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver,
17+
@inject(RenameTypeDetector) private renameTypeDetector: RenameTypeDetector,
18+
) {
19+
}
20+
21+
public async execute({ document, position, newName }: Props): Promise<void> {
22+
if (!newName.length) {
23+
throw new Error('New name cannot be empty');
24+
}
25+
26+
const type = this.renameTypeDetector.execute({ document, position });
27+
28+
const currentUri = document.uri;
29+
const oldPath = currentUri.fsPath;
30+
const fileName = this.workspacePathResolver.extractClassNameFromPath(oldPath);
31+
32+
if (type === 'namespace') {
33+
try {
34+
const newDirectoryPath = await this.workspacePathResolver.getDirectoryFromNamespace(newName);
35+
36+
FileRenameHandler.create({
37+
oldUri: currentUri,
38+
newUri: Uri.file(`${newDirectoryPath}/${fileName}.php`),
39+
});
40+
return;
41+
} catch (error) {
42+
window.showErrorMessage(`Error renaming namespace: ${error}`);
43+
}
44+
}
45+
46+
const directory = oldPath.substring(0, oldPath.lastIndexOf('/'));
47+
const extension = oldPath.substring(oldPath.lastIndexOf('.'));
48+
49+
FileRenameHandler.create({
50+
oldUri: currentUri,
51+
newUri: Uri.file(`${directory}/${fileName}${extension}`),
52+
});
53+
}
54+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Position, TextDocument } from 'vscode';
2+
import { injectable } from 'tsyringe';
3+
4+
interface Props {
5+
document: TextDocument
6+
position: Position
7+
}
8+
9+
type TypeRename = 'namespace' | 'class';
10+
11+
@injectable()
12+
export class RenameTypeDetector {
13+
public execute({ document, position }: Props): TypeRename {
14+
const text = document.getText();
15+
const lines = text.split('\n');
16+
const currentLine = lines[position.line] ?? '';
17+
18+
if (currentLine.match(/^\s*namespace\s+/)) {
19+
return 'namespace';
20+
}
21+
22+
if (currentLine.match(/^\s*(?:abstract\s+)?(?:final\s+)?(?:class|interface|trait)\s+/)) {
23+
return 'class';
24+
}
25+
26+
throw new Error('Type rename not identified');
27+
}
28+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { injectable } from 'tsyringe';
2+
3+
const NAMESPACE_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(\\[A-Za-z_][A-Za-z0-9_]*)*$/;
4+
5+
@injectable()
6+
export class RenameValidator {
7+
public validate(value: string): string | null {
8+
if (!value || value.trim() === '') {
9+
return 'Name cannot be empty';
10+
}
11+
12+
if (value.startsWith('\\')) {
13+
return 'Cannot start with \\';
14+
}
15+
16+
if (value.endsWith('\\')) {
17+
return 'Cannot end with \\';
18+
}
19+
20+
if (value.includes('\\\\')) {
21+
return 'Cannot contain double \\\\';
22+
}
23+
24+
if (!/^[A-Za-z]/.test(value)) {
25+
return 'Must start with a letter';
26+
}
27+
28+
if (!NAMESPACE_REGEX.test(value)) {
29+
return 'Invalid format. Use: Foo\\Bar';
30+
}
31+
32+
return null;
33+
}
34+
}

src/app/services/update/ClassNameUpdater.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Range, Uri, workspace, WorkspaceEdit } from "vscode";
33
import { TextDocumentOpener } from "../TextDocumentOpener";
44
import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver';
55

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

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

22-
const match = CLASS_REGEX.exec(text);
22+
const match = PHP_CLASS_DECLARATION_REGEX.exec(text);
2323
if (!match) {
2424
return;
2525
}

0 commit comments

Comments
 (0)