diff --git a/Sample/SampleProject/BaseClass.cs b/Sample/SampleProject/BaseClass.cs index a756af0..efa05dd 100644 --- a/Sample/SampleProject/BaseClass.cs +++ b/Sample/SampleProject/BaseClass.cs @@ -8,17 +8,5 @@ namespace Sample { public class BaseClass : IBaseClass { - public string FullPropertyAlt - { - get - { - return _fullProperty; - } - set - { - _fullProperty = value; - } - } - public int MyProperty { get; set; } } } diff --git a/Sample/SampleProject/MyClass.cs b/Sample/SampleProject/MyClass.cs index af15646..ef54609 100644 --- a/Sample/SampleProject/MyClass.cs +++ b/Sample/SampleProject/MyClass.cs @@ -15,6 +15,18 @@ public string FullProperty get => _fullProperty; set => _fullProperty = value; } + public string FullPropertyAlt + { + get + { + return _fullProperty; + } + set + { + _fullProperty = value; + } + } + public int MyProperty { get; set; } public async Task GetNewIdAsync(string name, string address, string city, diff --git a/package.json b/package.json index 9b9905c..f5ef2d2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publisher": "DanTurco", "displayName": "PullToInterfacor", "description": "A Visual Studio Code Extension to include the ability to **Pull** methods and properties to inherited interfaces and base classes. This is targeted to C# development and is meant as a supplemental extension to C# Dev Kit. This extension supports pulling public properties and methods to interfaces, and public and protected methods to base classes.", - "version": "1.0.0", + "version": "1.0.1", "engines": { "vscode": "^1.85.0" }, @@ -46,7 +46,7 @@ "lint": "eslint src --ext ts", "lint-fix": "eslint src --ext ts --fix", "test": "vscode-test", - "test-jest": "jest", + "test-jest": "jest --verbose false", "test-jest-watch": "jest --watch", "test-jest-coverage": "jest --coverage", "test:integration": "npm run compile && node ./node_modules/vscode/bin/test", diff --git a/src/extension.ts b/src/extension.ts index db8d681..f52fd62 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,10 @@ -import { WorkspaceFolder } from 'vscode'; // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; import { getWorkspaceFolder } from './utils/workspace-util'; import * as csharp from './pull-to-interface-csharp'; import { IWindow } from './interfaces/window.interface'; -import { SignatureLineResult, SignatureType, checkIfAlreadyPulledToInterface, cleanExcessiveNewLines, getLineEnding, getMemberBodyByBrackets, getMemberBodyBySemiColon, getMemberName, getUsingStatements } from './utils/csharp-util'; +import { SignatureLineResult, SignatureType, addLineBetweenMembers, checkIfAlreadyPulledToInterface, formatTextWithProperNewLines, getLineEnding, getMemberBodyByBrackets, getMemberBodyBySemiColon, getMemberName, getUsingStatements } from './utils/csharp-util'; const extensionName = 'pulltointerfacor.pullto'; @@ -15,9 +14,9 @@ const extensionName = 'pulltointerfacor.pullto'; export async function activate(context: vscode.ExtensionContext) { var wsf = vscode.workspace.workspaceFolders; - const workspaceRoot: string = getWorkspaceFolder(wsf as WorkspaceFolder[]); + const workspaceRoot: string = getWorkspaceFolder(wsf as vscode.WorkspaceFolder[]); - let disposable = vscode.commands.registerTextEditorCommand(extensionName, async (editor) => + let disposable = vscode.commands.registerTextEditorCommand(extensionName, async (editor: vscode.TextEditor) => { if (editor && editor.document.languageId === 'csharp') { @@ -54,7 +53,7 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension const isRegistered = await isSubcommandRegisteredAsync(subCommandName); if (!isRegistered) { - const disposable = vscode.commands.registerTextEditorCommand(subCommandName, async (editor) => + const disposable = vscode.commands.registerTextEditorCommand(subCommandName, async (editor: vscode.TextEditor) => { const signatureResult = csharp.getSignatureToPull(editor, '(public|protected)'); let methodBodySignature: SignatureLineResult | null = null; @@ -65,7 +64,7 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension vscode.window.showErrorMessage(`Unsupported pull. Unable to determine what to pull. 'public' properties and 'public' or 'protected' methods are only supported. Please copy manually`); return; } - + const tokenSource = new vscode.CancellationTokenSource(); //read file contents const files = await vscode.workspace.findFiles(`**/${subcommand}.cs`, '**/node_modules/**'); if (files.length > 1) @@ -73,6 +72,11 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension vscode.window.showErrorMessage(`More then one file found matching ${subcommand}. Please copy manually`); return; } + if (files.length === 0) + { + vscode.window.showErrorMessage(`No files found matching ${subcommand}. Please copy manually`); + return; + } const eol = getLineEnding(editor); const selectedFileDocument = await vscode.workspace.openTextDocument(files[0].path); let selectedFileDocumentContent = selectedFileDocument.getText(); @@ -118,10 +122,10 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension if (selectedFileDocumentContent) { - const currentDocumentUsings = getUsingStatements(editor); + const currentDocumentUsings = getUsingStatements(editor, eol); selectedFileDocumentContent = csharp.addUsingsToDocument(eol, selectedFileDocumentContent, currentDocumentUsings); - selectedFileDocumentContent = cleanExcessiveNewLines(selectedFileDocumentContent, eol); - + selectedFileDocumentContent = formatTextWithProperNewLines(selectedFileDocumentContent, eol); + selectedFileDocumentContent = addLineBetweenMembers(selectedFileDocumentContent,eol); const success = await csharp.applyEditsAsync(files[0].path, selectedFileDocumentContent); if (!success) { @@ -138,12 +142,9 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension if (!subcommand.startsWith("I") && methodBodySignature?.signature) { //remove if from current file - const activeFileUrl = editor.document.uri; + const activeFileUrl = editor.document.uri.path; const currentFileDocument = await vscode.workspace.openTextDocument(activeFileUrl); - let currentFileDocumentContent = currentFileDocument.getText(); - currentFileDocumentContent = currentFileDocumentContent.replace(methodBodySignature.signature + eol, ''); - currentFileDocumentContent = cleanExcessiveNewLines(currentFileDocumentContent, eol); - const success = await csharp.applyEditsAsync(activeFileUrl.path, currentFileDocumentContent); + const success = await removeFromCurrentEditorAsync(currentFileDocument, methodBodySignature, eol, activeFileUrl); if (!success) { vscode.window.showErrorMessage(`Unable to remove ${methodBodySignature.signatureType}. Please remove manually`); @@ -179,6 +180,16 @@ const buildSubCommands = async (subcommands: string[], context: vscode.Extension } }; +async function removeFromCurrentEditorAsync(currentFileDocument: vscode.TextDocument, methodBodySignature: SignatureLineResult, eol: string, activeFileUrl: string) +{ + let currentFileDocumentContent = currentFileDocument.getText(); + currentFileDocumentContent = currentFileDocumentContent.replace(methodBodySignature.signature + eol, ''); + currentFileDocumentContent = formatTextWithProperNewLines(currentFileDocumentContent, eol); + currentFileDocumentContent = addLineBetweenMembers(currentFileDocumentContent,eol); + const success = await csharp.applyEditsAsync(activeFileUrl, currentFileDocumentContent); + return success; +} + // This method is called when your extension is deactivated export function deactivate() { } diff --git a/src/pull-to-interface-csharp.ts b/src/pull-to-interface-csharp.ts index 4d08642..be72c20 100644 --- a/src/pull-to-interface-csharp.ts +++ b/src/pull-to-interface-csharp.ts @@ -52,6 +52,10 @@ export const getSubCommandsAsync = async (workspaceRoot: string, window: IWindow const openFileAndReadInheritedNamesAsync = async (fileName: string, tracker: InheritedMemberTracker): Promise => { const files = await workspace.findFiles(`**/${fileName}.cs`, '**/node_modules/**'); + if (files.length > 1 || files.length === 0) + { + return []; + } const document = await workspace.openTextDocument(files[0].path); const text = document.getText(); const inheritedNames = getInheritedNames(text, true); @@ -167,7 +171,7 @@ export const addUsingsToDocument = ( return documentFileContent; } //add the usings to file content - const existingDocumentUsings = getUsingStatementsFromText(documentFileContent); + const existingDocumentUsings = getUsingStatementsFromText(documentFileContent, eol); let combinedUsings = [...usings, ...existingDocumentUsings]; combinedUsings = [...new Set(combinedUsings)]; //distinct documentFileContent = replaceUsingStatementsFromText(documentFileContent, combinedUsings, eol); diff --git a/src/test/test-class.ts b/src/test/test-class.ts index 9003b94..ac33dc9 100644 --- a/src/test/test-class.ts +++ b/src/test/test-class.ts @@ -117,3 +117,159 @@ namespace Sample } } `; + + +export const testTextWithProperNewLines = `using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + + + + +namespace Sample +{ + public class MyClass : BaseClass, IMyClass, IMyTypedClass where TType : class + { + private string fullProperty; + + + public int MyProperty { get; set; } + + + + public int MyPropertyLamda => 5; + + + + public string FullProperty + { + get => fullProperty; + set => fullProperty = value; + } + + + + + public string FullPropertyAlt + { + get + { + return fullProperty; + } + set + { + fullProperty = value; + } + } + } +} +`; + +export const testTextWithProperNewLinesExpected = `using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample +{ + public class MyClass : BaseClass, IMyClass, IMyTypedClass where TType : class + { + private string fullProperty; + + public int MyProperty { get; set; } + + public int MyPropertyLamda => 5; + + public string FullProperty + { + get => fullProperty; + set => fullProperty = value; + } + + public string FullPropertyAlt + { + get + { + return fullProperty; + } + set + { + fullProperty = value; + } + } + } +} +`; + + + + + +export const testAddLinesBetweenMembers = `using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample +{ + public class MyClass : BaseClass, IMyClass, IMyTypedClass where TType : class + { + private string fullProperty; + public int MyProperty { get; set; } + public int MyPropertyLamda => 5; + public string FullProperty + { + get => fullProperty; + set => fullProperty = value; + } + public string FullPropertyAlt + { + get + { + return fullProperty; + } + set + { + fullProperty = value; + } + } + } +} +`; + +export const testAddLinesBetweenMembersExpected = `using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample +{ + public class MyClass : BaseClass, IMyClass, IMyTypedClass where TType : class + { + private string fullProperty; + + public int MyProperty { get; set; } + + public int MyPropertyLamda => 5; + + public string FullProperty + { + get => fullProperty; + set => fullProperty = value; + } + + public string FullPropertyAlt + { + get + { + return fullProperty; + } + set + { + fullProperty = value; + } + } + } +} +`; diff --git a/src/utils/csharp-util.test.ts b/src/utils/csharp-util.test.ts index eea8929..17f4250 100644 --- a/src/utils/csharp-util.test.ts +++ b/src/utils/csharp-util.test.ts @@ -1,9 +1,9 @@ import { Position, Selection, Uri } from 'vscode'; -import { getClassName, getCurrentLine, getInheritedNames, getNamespace, getFullSignatureOfLine, isMethod, isValidAccessorLine, isTerminating, SignatureType, getLineEnding, getUsingStatements, replaceUsingStatementsFromText, getUsingStatementsFromText, getMemberName, getMemberBodyByBrackets, getMemberBodyBySemiColon } from './csharp-util'; +import { getClassName, getCurrentLine, getInheritedNames, getNamespace, getFullSignatureOfLine, isMethod, isValidAccessorLine, isTerminating, SignatureType, getLineEnding, getUsingStatements, replaceUsingStatementsFromText, getUsingStatementsFromText, getMemberName, getMemberBodyByBrackets, getMemberBodyBySemiColon, formatTextWithProperNewLines, getLineEndingFromDoc, addLineBetweenMembers } from './csharp-util'; import * as vscodeMock from 'jest-mock-vscode'; import { MockTextEditor } from 'jest-mock-vscode/dist/vscode'; -import { testFile } from '../test/test-class'; +import { testAddLinesBetweenMembers, testAddLinesBetweenMembersExpected, testFile, testTextWithProperNewLines, testTextWithProperNewLinesExpected} from '../test/test-class'; describe('CSharp Util', () => @@ -502,10 +502,10 @@ describe('CSharp Util', () => { var doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), testFile, 'csharp'); const editor = new MockTextEditor(jest, doc, undefined, new Selection(new Position(1, 0), new Position(1, 0))); - - const result = getUsingStatements(editor); + const eol = getLineEndingFromDoc(doc); + const result = getUsingStatements(editor, eol); expect(result).toHaveLength(4); - expect(result[0]).toEqual('using System;\n'); + expect(result[0]).toEqual('using System;'); }); }); @@ -514,13 +514,38 @@ describe('CSharp Util', () => it('should return array of using statements', () => { var doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), testFile, 'csharp'); - const result = replaceUsingStatementsFromText(doc.getText(), ['using NoMatch;'], '\n'); + const eol = getLineEndingFromDoc(doc); + const result = replaceUsingStatementsFromText(doc.getText(), ['using NoMatch;'], eol); expect(result).toContain('using NoMatch;'); - const items = getUsingStatementsFromText(result); + const items = getUsingStatementsFromText(result, eol); expect(items).toHaveLength(1); }); }); + describe('formatTextWithProperNewLines', () => + { + it('should return spaced out body members', () => + { + var doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), testTextWithProperNewLines, 'csharp'); + const eol = getLineEndingFromDoc(doc); + const result = formatTextWithProperNewLines(doc.getText(), eol); + expect(result).toEqual(testTextWithProperNewLinesExpected); + }); + }); + + describe('addLineBetweenMembers', () => + { + it.only('should return spaced out body members', () => + { + var doc = vscodeMock.createTextDocument(Uri.parse('C:\temp\test.cs'), testAddLinesBetweenMembers, 'csharp'); + const eol = getLineEndingFromDoc(doc); + const result = addLineBetweenMembers(doc.getText(), eol); + const sp = result.replace(/ /g, ''); + const ep = testAddLinesBetweenMembersExpected.replace(/ /g, ''); + expect(sp).toEqual(ep); + }); + }); + describe('getMemberBodyByBrackets', () => { it('should return the full method body', () => diff --git a/src/utils/csharp-util.ts b/src/utils/csharp-util.ts index cd07e49..b1fe91c 100644 --- a/src/utils/csharp-util.ts +++ b/src/utils/csharp-util.ts @@ -1,4 +1,4 @@ -import { EndOfLine, TextEditor } from 'vscode'; +import { EndOfLine, TextDocument, TextEditor } from 'vscode'; import { IWindow } from '../interfaces/window.interface'; export type PublicProtected = 'public' | 'protected'; @@ -308,7 +308,11 @@ export const isTerminating = (currentLine: string, includeClosingBracket: boolea export const getLineEnding = (editor: TextEditor): string => { - const document = editor.document; + return getLineEndingFromDoc(editor.document); +}; + +export const getLineEndingFromDoc = (document: TextDocument): string => +{ if (EndOfLine.CRLF === document.eol) { return '\r\n'; @@ -316,27 +320,34 @@ export const getLineEnding = (editor: TextEditor): string => return '\n'; }; -export const getUsingStatements = (editor: TextEditor): string[] => +export const convertEndOfLine = (eol: EndOfLine): string => +{ + if (EndOfLine.CRLF === eol) + { + return '\r\n'; + } + return '\n'; +}; + +export const getUsingStatements = (editor: TextEditor, eol: string): string[] => { const document = editor.document; const docText = document.getText(); - return getUsingStatementsFromText(docText); + return getUsingStatementsFromText(docText, eol); }; -export const getUsingStatementsFromText = (docText: string): string[] => +export const getUsingStatementsFromText = (docText: string, eol: string): string[] => { - const usingRegex = new RegExp('using.*;[\r\n*]', 'gm'); - const matches = docText.match(usingRegex); - return matches?.map(m => m) || []; + let lines = docText.split(eol); + return lines.filter(f => f.startsWith('using')); }; export const replaceUsingStatementsFromText = (docText: string, newUsings: string[], eol: string): string => { - const usingRegex = new RegExp('^using.*;[\r\n*]', 'gm'); - const u = newUsings.join(''); - let cleared = docText.replace(usingRegex, ''); - cleared = u + eol + cleared; - return cleared; + let lines = docText.split(eol); + lines = lines.filter(f => !f.startsWith('using')); + lines = [...newUsings, ...lines]; + return lines.join(eol); }; export const getBeginningOfLineIndent = (text: string): number => @@ -350,11 +361,57 @@ export const getBeginningOfLineIndent = (text: string): number => return 0; }; -export const cleanExcessiveNewLines = (text: string, eol: string): string => +export const addLineBetweenMembers = (text: string, eol: string): string => +{ + //const bracketAccessorRegex = new RegExp('(}|;)(\\s)(public|protected|private|internal)', 'gm'); + //return text.replace(bracketAccessorRegex, `$1${eol}$2$3`); + + let lines = text.split(eol); + let resultLines: string[] = []; + for (let index = 0; index < lines.length; index++) + { + const line = lines[index]; + resultLines.push(line); + if ((line.trim() === "}" || line.trim().endsWith("}") || line.trim().endsWith(";")) && index + 1 <= lines.length && lines[index + 1].trim().match(/(public|protected|private|internal)/)) + { + resultLines.push(''); + } + } + return resultLines.join(eol); +}; + +export const formatTextWithProperNewLines = (text: string, eol: string): string => { - const newlineRegex = /^\s{2,}$/gm; - text = text.replace(newlineRegex, ''); - return text.replace('namespace', `${eol}namespace`); + let lines = text.split(eol); + let resultLines: string[] = []; + let foundEmptyLine = false; + + const crlf = convertEndOfLine(EndOfLine.CRLF); + const lf = convertEndOfLine(EndOfLine.LF); + for (let line of lines) + { + if (line.endsWith(crlf) || line.endsWith(lf)) + { + line = line.replace(crlf, '').replace(lf, ''); + } + if (line.trim() === "") + { + // Found an empty line + if (!foundEmptyLine) + { + // Add the first empty line to the result + resultLines.push(line); + foundEmptyLine = true; + } + } + else + { + // Found a line with text + resultLines.push(line); + foundEmptyLine = false; + } + } + return resultLines.join(eol); }; @@ -371,5 +428,5 @@ export const checkIfAlreadyPulledToInterface = (text: string, signatureResult: S export const cleanAllAccessors = (text: string): string => { let regex = new RegExp(`((public|private|protected|internal|abstract|virtual|override)[\\s]*)`, 'gm'); - return text.replace(regex,''); + return text.replace(regex, ''); };