├── src
├── Templates
│ ├── .json.txt
│ ├── .md.txt
│ ├── .bowerrc.txt
│ ├── gulpfile.js.txt
│ ├── .vb.txt
│ ├── .razor.txt
│ ├── .vb-interface.txt
│ ├── gruntfile.js.txt
│ ├── .cs-enum.txt
│ ├── bower.json.txt
│ ├── .html.txt
│ ├── package.json.txt
│ ├── .cs-controller.txt
│ ├── .cs.txt
│ ├── .cs-interface.txt
│ └── .linq.txt
├── Resources
│ ├── icon.png
│ └── logo.png
├── Properties
│ └── AssemblyInfo.cs
├── source.extension.cs
├── Helpers
│ ├── Logger.cs
│ ├── VsTheme.cs
│ └── ProjectHelpers.cs
├── AddAnyFile.cs
├── FileNameDialog.xaml
├── source.extension.vsixmanifest
├── AddAnyFile.vsct
├── FileNameDialog.xaml.cs
├── NewItemTarget.cs
├── Templatemap.cs
├── AddAnyFile.csproj
└── AddAnyFilePackage.cs
├── art
├── menu.png
└── dialog.png
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── workflows
│ └── build.yaml
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── vs-publish.json
├── .gitattributes
├── AddAnyFile.sln
├── README.md
├── .editorconfig
├── .gitignore
└── LICENSE
/src/Templates/.json.txt:
--------------------------------------------------------------------------------
1 | {
2 | $
3 | }
--------------------------------------------------------------------------------
/src/Templates/.md.txt:
--------------------------------------------------------------------------------
1 | # Markdown File
2 |
3 | $
--------------------------------------------------------------------------------
/src/Templates/.bowerrc.txt:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "wwwroot/lib"$
3 | }
--------------------------------------------------------------------------------
/src/Templates/gulpfile.js.txt:
--------------------------------------------------------------------------------
1 | var gulp = require("gulp");
2 |
3 | $
--------------------------------------------------------------------------------
/src/Templates/.vb.txt:
--------------------------------------------------------------------------------
1 | Public Class {itemname}
2 | $
3 | End Class
4 |
--------------------------------------------------------------------------------
/art/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/AddAnyFile/master/art/menu.png
--------------------------------------------------------------------------------
/src/Templates/.razor.txt:
--------------------------------------------------------------------------------
1 |
{itemname}
2 |
3 | $
4 |
5 | @code {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/art/dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/AddAnyFile/master/art/dialog.png
--------------------------------------------------------------------------------
/src/Templates/.vb-interface.txt:
--------------------------------------------------------------------------------
1 | Public Interface {itemname}
2 | $
3 | End Interface
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: madskristensen
4 |
--------------------------------------------------------------------------------
/src/Resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/AddAnyFile/master/src/Resources/icon.png
--------------------------------------------------------------------------------
/src/Resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/AddAnyFile/master/src/Resources/logo.png
--------------------------------------------------------------------------------
/src/Templates/gruntfile.js.txt:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | grunt.initConfig({
3 | $
4 | });
5 | };
--------------------------------------------------------------------------------
/src/Templates/.cs-enum.txt:
--------------------------------------------------------------------------------
1 | namespace {namespace}
2 | {
3 | public enum {itemname}
4 | {
5 | $
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Templates/bower.json.txt:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mypackage",
3 | "private": true,
4 | "dependencies": {
5 | $
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Templates/.html.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {itemname}
5 |
6 |
7 | $
8 |
9 |
--------------------------------------------------------------------------------
/src/Templates/package.json.txt:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "name": "mypackage",
4 | "private": true,
5 | "devDependencies": {
6 | $
7 | }
8 | }
--------------------------------------------------------------------------------
/src/Templates/.cs-controller.txt:
--------------------------------------------------------------------------------
1 | using {mvcprojectnamespace};
2 |
3 | namespace {namespace}
4 | {
5 | public class {itemname} : Controller
6 | {
7 | $
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Templates/.cs.txt:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace {namespace}
8 | {
9 | public class {itemname}
10 | {
11 | $
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Templates/.cs-interface.txt:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace {namespace}
8 | {
9 | public interface {itemname}
10 | {
11 | $
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Description
2 | [Description of the bug or feature]
3 |
4 | ### Steps to reproduce
5 | 1. [First Step]
6 | 2. [Second Step]
7 | 3. [and so on...]
8 |
9 | **Expected behavior:** [What you expected to happen]
10 |
11 | **Actual behavior:** [What actually happened]
--------------------------------------------------------------------------------
/src/Templates/.linq.txt:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace {namespace}
9 | {
10 | public class {itemname}
11 | {
12 | $
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/vs-publish.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/vsix-publish",
3 | "categories": [ "other", "coding" ],
4 | "identity": {
5 | "internalName": "AddNewFile64"
6 | },
7 | "assetFiles": [
8 | {
9 | "pathOnDisk": "art/dialog.png",
10 | "targetPath": "art/dialog.png"
11 | },
12 | {
13 | "pathOnDisk": "art/menu.png",
14 | "targetPath": "art/menu.png"
15 | }
16 | ],
17 | "overview": "README.md",
18 | "publisher": "MadsKristensen",
19 | "repo": "https://github.com/madskristensen/AddAnyFile"
20 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/src/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Resources;
4 | using System.Runtime.InteropServices;
5 | using MadsKristensen.AddAnyFile;
6 |
7 | [assembly: AssemblyTitle(Vsix.Name)]
8 | [assembly: AssemblyDescription(Vsix.Description)]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany(Vsix.Author)]
11 | [assembly: AssemblyProduct(Vsix.Name)]
12 | [assembly: AssemblyCopyright(Vsix.Author)]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 | [assembly: ComVisible(false)]
16 | [assembly: CLSCompliant(false)]
17 | [assembly: NeutralResourcesLanguage(Vsix.Language)]
18 |
19 | [assembly: AssemblyVersion(Vsix.Version)]
20 | [assembly: AssemblyFileVersion(Vsix.Version)]
--------------------------------------------------------------------------------
/src/source.extension.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // This file was generated by VSIX Synchronizer
4 | // Available from https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VsixSynchronizer64
5 | //
6 | // ------------------------------------------------------------------------------
7 | namespace MadsKristensen.AddAnyFile
8 | {
9 | internal sealed partial class Vsix
10 | {
11 | public const string Id = "d48e8f25-661b-4970-8b66-03f051ba5fc3";
12 | public const string Name = "Add New File (64-bit)";
13 | public const string Description = @"The fastest and easiest way to add new files to any project - including files that start with a dot";
14 | public const string Language = "en-US";
15 | public const string Version = "4.6";
16 | public const string Author = "Mads Kristensen";
17 | public const string Tags = "file, add, template";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Helpers/Logger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Microsoft.VisualStudio.Shell.Interop;
3 |
4 | using System;
5 |
6 | internal static class Logger
7 | {
8 | private static string _name;
9 | private static IVsOutputWindowPane _pane;
10 | private static IVsOutputWindow _output;
11 |
12 | public static void Initialize(IServiceProvider provider, string name)
13 | {
14 | _output = (IVsOutputWindow)provider.GetService(typeof(SVsOutputWindow));
15 | Assumes.Present(_output);
16 | _name = name;
17 | }
18 |
19 | public static void Log(object message)
20 | {
21 | try
22 | {
23 | if (EnsurePane())
24 | {
25 | _pane.OutputString(DateTime.Now.ToString() + ": " + message + Environment.NewLine);
26 | }
27 | }
28 | catch (Exception ex)
29 | {
30 | System.Diagnostics.Debug.Write(ex);
31 | }
32 | }
33 |
34 | private static bool EnsurePane()
35 | {
36 | if (_pane == null)
37 | {
38 | Guid guid = Guid.NewGuid();
39 | _output.CreatePane(ref guid, _name, 1, 1);
40 | _output.GetPane(ref guid, out _pane);
41 | }
42 |
43 | return _pane != null;
44 | }
45 | }
--------------------------------------------------------------------------------
/src/AddAnyFile.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // This file was generated by VSIX Synchronizer
4 | //
5 | // ------------------------------------------------------------------------------
6 | namespace MadsKristensen.AddAnyFile
7 | {
8 | using System;
9 |
10 | ///
11 | /// Helper class that exposes all GUIDs used across VS Package.
12 | ///
13 | internal sealed partial class PackageGuids
14 | {
15 | public const string guidAddAnyFilePkgString = "27dd9dea-6dd2-403e-929d-3ff20d896c5e";
16 | public static Guid guidAddAnyFilePkg = new Guid(guidAddAnyFilePkgString);
17 |
18 | public const string guidAddAnyFileCmdSetString = "32af8a17-bbbc-4c56-877e-fc6c6575a8cf";
19 | public static Guid guidAddAnyFileCmdSet = new Guid(guidAddAnyFileCmdSetString);
20 | }
21 | ///
22 | /// Helper class that encapsulates all CommandIDs uses across VS Package.
23 | ///
24 | internal sealed partial class PackageIds
25 | {
26 | public const int cmdidMyCommand = 0x0100;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/FileNameDialog.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/source.extension.vsixmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Add New File (64-bit)
6 | The fastest and easiest way to add new files to any project - including files that start with a dot
7 | https://github.com/madskristensen/AddAnyFile
8 | Resources\LICENSE
9 | Resources\logo.png
10 | Resources\logo.png
11 | file, add, template
12 |
13 |
14 |
15 | amd64
16 |
17 |
18 | arm64
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/AddAnyFile.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.32017.224
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AddAnyFile", "src\AddAnyFile.csproj", "{CAD947D3-06E2-4A76-8838-68115036B179}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50C409A6-1C74-4C72-887A-3AC964BD505D}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | .github\workflows\build.yaml = .github\workflows\build.yaml
12 | README.md = README.md
13 | vs-publish.json = vs-publish.json
14 | EndProjectSection
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | CI|Any CPU = CI|Any CPU
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {CAD947D3-06E2-4A76-8838-68115036B179}.CI|Any CPU.ActiveCfg = CI|Any CPU
24 | {CAD947D3-06E2-4A76-8838-68115036B179}.CI|Any CPU.Build.0 = CI|Any CPU
25 | {CAD947D3-06E2-4A76-8838-68115036B179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {CAD947D3-06E2-4A76-8838-68115036B179}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {CAD947D3-06E2-4A76-8838-68115036B179}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {CAD947D3-06E2-4A76-8838-68115036B179}.Release|Any CPU.Build.0 = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(SolutionProperties) = preSolution
31 | HideSolutionNode = FALSE
32 | EndGlobalSection
33 | GlobalSection(ExtensibilityGlobals) = postSolution
34 | SolutionGuid = {5AAC7DA5-C310-4BAD-9A85-AADC50C3BE18}
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/src/AddAnyFile.vsct:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/FileNameDialog.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Windows;
4 | using System.Windows.Input;
5 | using System.Windows.Media.Imaging;
6 |
7 | namespace MadsKristensen.AddAnyFile
8 | {
9 | public partial class FileNameDialog : Window
10 | {
11 | private const string DEFAULT_TEXT = "Enter a file name";
12 | private static readonly List _tips = new List {
13 | "Tip: 'folder/file.ext' also creates a new folder for the file",
14 | "Tip: You can create files starting with a dot, like '.gitignore'",
15 | "Tip: You can create files without file extensions, like 'LICENSE'",
16 | "Tip: Create folder by ending the name with a forward slash",
17 | "Tip: Use glob style syntax to add related files, like 'widget.(html,js)'",
18 | "Tip: Separate names with commas to add multiple files and folders"
19 | };
20 |
21 | public FileNameDialog(string folder)
22 | {
23 | InitializeComponent();
24 |
25 | lblFolder.Content = string.Format("{0}/", folder);
26 |
27 | Loaded += (s, e) =>
28 | {
29 | Icon = BitmapFrame.Create(new Uri("pack://application:,,,/AddAnyFile;component/Resources/icon.png", UriKind.RelativeOrAbsolute));
30 | Title = Vsix.Name;
31 | SetRandomTip();
32 |
33 | txtName.Focus();
34 | txtName.CaretIndex = 0;
35 | txtName.Text = DEFAULT_TEXT;
36 | txtName.Select(0, txtName.Text.Length);
37 |
38 | txtName.PreviewKeyDown += (a, b) =>
39 | {
40 | if (b.Key == Key.Escape)
41 | {
42 | if (string.IsNullOrWhiteSpace(txtName.Text) || txtName.Text == DEFAULT_TEXT)
43 | {
44 | Close();
45 | }
46 | else
47 | {
48 | txtName.Text = string.Empty;
49 | }
50 | }
51 | else if (txtName.Text == DEFAULT_TEXT)
52 | {
53 | txtName.Text = string.Empty;
54 | btnCreate.IsEnabled = true;
55 | }
56 | };
57 |
58 | };
59 | }
60 |
61 | public string Input => txtName.Text.Trim();
62 |
63 | private void SetRandomTip()
64 | {
65 | Random rnd = new Random(DateTime.Now.GetHashCode());
66 | int index = rnd.Next(_tips.Count);
67 | lblTips.Content = _tips[index];
68 | }
69 |
70 | private void Button_Click(object sender, RoutedEventArgs e)
71 | {
72 | DialogResult = true;
73 | Close();
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Add Any File
2 |
3 | **Download** this extension from the [VS Marketplace](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.AddNewFile64)
4 | or get the [CI build](https://www.vsixgallery.com/extension/d48e8f25-661b-4970-8b66-03f051ba5fc3).
5 |
6 | -----------------------------------
7 |
8 | A Visual Studio extension for easily adding new files to any project. Simply hit Shift+F2 to create an empty file in the
9 | selected folder or in the same folder as the selected file.
10 |
11 | ### Features
12 |
13 | - Easily create any file with any file extension
14 | - Create files starting with a dot like `.gitignore`
15 | - Create deeper folder structures easily if required
16 | - Create folders when the entered name ends with a /
17 |
18 | 
19 |
20 | ### Show the dialog
21 |
22 | A new button is added to the context menu in Solution Explorer.
23 |
24 | 
25 |
26 | You can either click that button or use the keybord shortcut **Shift+F2**.
27 |
28 | ### Create folders
29 |
30 | Create additional folders for your file by using forward-slash to
31 | specify the structure.
32 |
33 | For example, by typing **scripts/test.js** in the dialog, the
34 | folder **scripts** is created if it doesn't exist and the file
35 | **test.js** is then placed into it.
36 |
37 | ### Custom templates
38 |
39 | Create a `.templates` folder at the root of your project.
40 | The templates inside this folder will be used alongside the default ones.
41 |
42 | #### Keywords
43 | Inside the template those keywords can be used:
44 | - `{itemname}`: The name of the file without the extension
45 | - `{namespace}`: The namespace
46 |
47 | #### Types of template
48 | 3 types of template are available:
49 |
50 | ##### Exact match
51 | When creating the file `Dockerfile`, the extension will look for `dockerfile.txt` template.
52 |
53 | ##### Convention match or Partial match
54 | If you create a template with the name `repository.txt`, then it will be used when creating a file ending with `Repository` (eg: DataRepository).
55 |
56 | ##### Extension match
57 | When creating the file `Test.cs`, the extension will look for `.cs.txt` template.
58 |
59 | ### How can I help?
60 | If you enjoy using the extension, please give it a ????? rating on the [Visual Studio Marketplace][marketplace].
61 |
62 | Should you encounter bugs or if you have feature requests, head on over to the [GitHub repo][repo] to open an issue if one doesn't already exist.
63 |
64 | Pull requests are also very welcome, since I can't always get around to fixing all bugs myself. This is a personal passion project, so my time is limited.
65 |
66 | Another way to help out is to [sponser me on GitHub](https://github.com/sponsors/madskristensen).
67 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.vsct]
2 |
3 | indent_style = space
4 | indent_size = 4
5 | charset = utf-8-bom
6 |
7 | [*.cs]
8 |
9 | #Core editorconfig formatting - indentation
10 |
11 | indent_style = tab
12 | charset = utf-8-bom
13 |
14 | #Formatting - new line options
15 |
16 | csharp_new_line_before_catch = true
17 | csharp_new_line_before_else = true
18 | csharp_new_line_before_finally = true
19 | csharp_new_line_before_members_in_object_initializers = true
20 | csharp_new_line_before_open_brace = methods, control_blocks, object_collection_array_initializers, types
21 |
22 | #Formatting - organize using options
23 |
24 | dotnet_sort_system_directives_first = false
25 |
26 | #Formatting - spacing options
27 |
28 | csharp_space_after_cast = false
29 | csharp_space_after_colon_in_inheritance_clause = true
30 | csharp_space_after_keywords_in_control_flow_statements = true
31 | csharp_space_before_colon_in_inheritance_clause = true
32 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
33 | csharp_space_between_method_call_name_and_opening_parenthesis = false
34 | csharp_space_between_method_call_parameter_list_parentheses = false
35 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
36 | csharp_space_between_method_declaration_parameter_list_parentheses = false
37 |
38 | #Formatting - wrapping options
39 |
40 | csharp_preserve_single_line_blocks = true
41 |
42 | #Style - Code block preferences
43 |
44 | csharp_prefer_braces = true:suggestion
45 |
46 | #Style - expression bodied member options
47 |
48 | csharp_style_expression_bodied_methods = false:suggestion
49 | csharp_style_expression_bodied_properties = true:suggestion
50 |
51 | #Style - expression level options
52 |
53 | csharp_style_inlined_variable_declaration = true:suggestion
54 | dotnet_style_predefined_type_for_member_access = true:suggestion
55 |
56 | #Style - Expression-level preferences
57 |
58 | dotnet_style_object_initializer = true:suggestion
59 |
60 | #Style - implicit and explicit types
61 |
62 | csharp_style_var_elsewhere = false:suggestion
63 | csharp_style_var_for_built_in_types = false:suggestion
64 | csharp_style_var_when_type_is_apparent = false:suggestion
65 |
66 | #Style - language keyword and framework type options
67 |
68 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
69 |
70 | #Style - modifier options
71 |
72 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
73 |
74 | #Style - Modifier preferences
75 |
76 | csharp_preferred_modifier_order = private,public,internal,protected,static,async,readonly,override:suggestion
77 |
78 | #Style - Pattern matching
79 |
80 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
81 |
82 | #Style - qualification options
83 |
84 | dotnet_style_qualification_for_method = false:suggestion
85 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2 | name: "Build"
3 |
4 | on:
5 | push:
6 | branches: [master]
7 | pull_request:
8 | branches: [master]
9 | workflow_dispatch:
10 | branches: [master]
11 |
12 | jobs:
13 | build:
14 | outputs:
15 | version: ${{ steps.vsix_version.outputs.version-number }}
16 | name: Build
17 | runs-on: windows-2022
18 | env:
19 | Configuration: Release
20 | DeployExtension: False
21 | VsixManifestPath: src\source.extension.vsixmanifest
22 | VsixManifestSourcePath: src\source.extension.cs
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 |
27 | - name: Setup .NET build dependencies
28 | uses: timheuer/bootstrap-dotnet@v1
29 | with:
30 | nuget: 'false'
31 | sdk: 'false'
32 | msbuild: 'true'
33 |
34 | - name: Increment VSIX version
35 | id: vsix_version
36 | uses: timheuer/vsix-version-stamp@v1
37 | with:
38 | manifest-file: ${{ env.VsixManifestPath }}
39 | vsix-token-source-file: ${{ env.VsixManifestSourcePath }}
40 |
41 | - name: Build
42 | run: msbuild /v:m -restore /p:OutDir=../_built
43 |
44 | #- name: Setup test
45 | # uses: darenm/Setup-VSTest@v1
46 |
47 | #- name: Test
48 | # run: vstest.console.exe _built\*test.dll
49 |
50 | - name: Upload artifact
51 | uses: actions/upload-artifact@v4
52 | with:
53 | name: ${{ github.event.repository.name }}.vsix
54 | path: _built/**/*.vsix
55 |
56 | publish:
57 | if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
58 | needs: build
59 | runs-on: windows-latest
60 |
61 | steps:
62 | - uses: actions/checkout@v2
63 |
64 | - name: Download Package artifact
65 | uses: actions/download-artifact@v4
66 | with:
67 | name: ${{ github.event.repository.name }}.vsix
68 |
69 | - name: Upload to Open VSIX
70 | uses: timheuer/openvsixpublish@v1
71 | with:
72 | vsix-file: ${{ github.event.repository.name }}.vsix
73 |
74 | - name: Tag and Release
75 | if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }}
76 | id: tag_release
77 | uses: softprops/action-gh-release@v0.1.13
78 | with:
79 | body: Release ${{ needs.build.outputs.version }}
80 | tag_name: ${{ needs.build.outputs.version }}
81 | files: |
82 | **/*.vsix
83 |
84 | - name: Publish extension to Marketplace
85 | if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[release]') }}
86 | uses: cezarypiatek/VsixPublisherAction@0.1
87 | with:
88 | extension-file: '${{ github.event.repository.name }}.vsix'
89 | publish-manifest-file: 'vs-publish.json'
90 | personal-access-code: ${{ secrets.VS_PUBLISHER_ACCESS_TOKEN }}
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 | .vs
9 |
10 | # Build results
11 | [Dd]ebug/
12 | [Dd]ebugPublic/
13 | [Rr]elease/
14 | x64/
15 | build/
16 | bld/
17 | [Bb]in/
18 | [Oo]bj/
19 |
20 | # MSTest test Results
21 | [Tt]est[Rr]esult*/
22 | [Bb]uild[Ll]og.*
23 |
24 | #NUNIT
25 | *.VisualState.xml
26 | TestResult.xml
27 |
28 | # Build Results of an ATL Project
29 | [Dd]ebugPS/
30 | [Rr]eleasePS/
31 | dlldata.c
32 |
33 | *_i.c
34 | *_p.c
35 | *_i.h
36 | *.ilk
37 | *.meta
38 | *.obj
39 | *.pch
40 | *.pdb
41 | *.pgc
42 | *.pgd
43 | *.rsp
44 | *.sbr
45 | *.tlb
46 | *.tli
47 | *.tlh
48 | *.tmp
49 | *.tmp_proj
50 | *.log
51 | *.vspscc
52 | *.vssscc
53 | .builds
54 | *.pidb
55 | *.svclog
56 | *.scc
57 |
58 | # Chutzpah Test files
59 | _Chutzpah*
60 |
61 | # Visual C++ cache files
62 | ipch/
63 | *.aps
64 | *.ncb
65 | *.opensdf
66 | *.sdf
67 | *.cachefile
68 |
69 | # Visual Studio profiler
70 | *.psess
71 | *.vsp
72 | *.vspx
73 |
74 | # TFS 2012 Local Workspace
75 | $tf/
76 |
77 | # Guidance Automation Toolkit
78 | *.gpState
79 |
80 | # ReSharper is a .NET coding add-in
81 | _ReSharper*/
82 | *.[Rr]e[Ss]harper
83 | *.DotSettings.user
84 |
85 | # JustCode is a .NET coding addin-in
86 | .JustCode
87 |
88 | # TeamCity is a build add-in
89 | _TeamCity*
90 |
91 | # DotCover is a Code Coverage Tool
92 | *.dotCover
93 |
94 | # NCrunch
95 | *.ncrunch*
96 | _NCrunch_*
97 | .*crunch*.local.xml
98 |
99 | # MightyMoose
100 | *.mm.*
101 | AutoTest.Net/
102 |
103 | # Web workbench (sass)
104 | .sass-cache/
105 |
106 | # Installshield output folder
107 | [Ee]xpress/
108 |
109 | # DocProject is a documentation generator add-in
110 | DocProject/buildhelp/
111 | DocProject/Help/*.HxT
112 | DocProject/Help/*.HxC
113 | DocProject/Help/*.hhc
114 | DocProject/Help/*.hhk
115 | DocProject/Help/*.hhp
116 | DocProject/Help/Html2
117 | DocProject/Help/html
118 |
119 | # Click-Once directory
120 | publish/
121 |
122 | # Publish Web Output
123 | *.[Pp]ublish.xml
124 | *.azurePubxml
125 |
126 | # NuGet Packages Directory
127 | packages/
128 | ## TODO: If the tool you use requires repositories.config uncomment the next line
129 | #!packages/repositories.config
130 |
131 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
132 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented)
133 | !packages/build/
134 |
135 | # Windows Azure Build Output
136 | csx/
137 | *.build.csdef
138 |
139 | # Windows Store app package directory
140 | AppPackages/
141 |
142 | # Others
143 | sql/
144 | *.Cache
145 | ClientBin/
146 | [Ss]tyle[Cc]op.*
147 | ~$*
148 | *~
149 | *.dbmdl
150 | *.dbproj.schemaview
151 | *.pfx
152 | *.publishsettings
153 | node_modules/
154 |
155 | # RIA/Silverlight projects
156 | Generated_Code/
157 |
158 | # Backup & report files from converting an old project file to a newer
159 | # Visual Studio version. Backup files are not needed, because we have git ;-)
160 | _UpgradeReport_Files/
161 | Backup*/
162 | UpgradeLog*.XML
163 | UpgradeLog*.htm
164 |
165 | # SQL Server files
166 | *.mdf
167 | *.ldf
168 |
169 | # Business Intelligence projects
170 | *.rdl.data
171 | *.bim.layout
172 | *.bim_*.settings
173 |
174 | # Microsoft Fakes
175 | FakesAssemblies/
176 |
177 | # =========================
178 | # Operating System Files
179 | # =========================
180 |
181 | # OSX
182 | # =========================
183 |
184 | .DS_Store
185 | .AppleDouble
186 | .LSOverride
187 |
188 | # Icon must ends with two \r.
189 | Icon
190 |
191 | # Thumbnails
192 | ._*
193 |
194 | # Files that might appear on external disk
195 | .Spotlight-V100
196 | .Trashes
197 |
198 | # Windows
199 | # =========================
200 |
201 | # Windows image file caches
202 | Thumbs.db
203 | ehthumbs.db
204 |
205 | # Folder config file
206 | Desktop.ini
207 |
208 | # Recycle Bin used on file shares
209 | $RECYCLE.BIN/
210 |
211 | # Windows Installer files
212 | *.cab
213 | *.msi
214 | *.msm
215 | *.msp
216 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | @mkristensen on Twitter.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/src/NewItemTarget.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 | using EnvDTE80;
3 | using Microsoft.VisualStudio.Shell;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 |
9 | namespace MadsKristensen.AddAnyFile
10 | {
11 | public class NewItemTarget
12 | {
13 | public static NewItemTarget Create(DTE2 dte)
14 | {
15 | NewItemTarget item = null;
16 |
17 | // If a document is active, try to use the document's containing directory.
18 | if (dte.ActiveWindow is Window2 window && window.Type == vsWindowType.vsWindowTypeDocument)
19 | {
20 | item = CreateFromActiveDocument(dte);
21 | }
22 |
23 | // If no document was selected, or we could not get a selected item from
24 | // the document, then use the selected item in the Solution Explorer window.
25 | if (item == null)
26 | {
27 | item = CreateFromSolutionExplorerSelection(dte);
28 | }
29 |
30 | return item;
31 | }
32 |
33 | private static NewItemTarget CreateFromActiveDocument(DTE2 dte)
34 | {
35 | string fileName = dte.ActiveDocument?.FullName;
36 | if (File.Exists(fileName))
37 | {
38 | ProjectItem docItem = dte.Solution.FindProjectItem(fileName);
39 | if (docItem != null)
40 | {
41 | return CreateFromProjectItem(docItem);
42 | }
43 | }
44 |
45 | return null;
46 | }
47 |
48 | private static NewItemTarget CreateFromSolutionExplorerSelection(DTE2 dte)
49 | {
50 | ThreadHelper.ThrowIfNotOnUIThread();
51 |
52 | Array items = (Array)dte.ToolWindows.SolutionExplorer.SelectedItems;
53 |
54 | if (items.Length == 1)
55 | {
56 | UIHierarchyItem selection = items.Cast().First();
57 |
58 | if (selection.Object is Solution solution)
59 | {
60 | return new NewItemTarget(Path.GetDirectoryName(solution.FullName), null, null, isSolutionOrSolutionFolder: true);
61 | }
62 | else if (selection.Object is Project project)
63 | {
64 | if (project.IsKind(Constants.vsProjectKindSolutionItems))
65 | {
66 | return new NewItemTarget(GetSolutionFolderPath(project), project, null, isSolutionOrSolutionFolder: true);
67 | }
68 | else
69 | {
70 | return new NewItemTarget(project.GetRootFolder(), project, null, isSolutionOrSolutionFolder: false);
71 | }
72 | }
73 | else if (selection.Object is ProjectItem projectItem)
74 | {
75 | return CreateFromProjectItem(projectItem);
76 | }
77 | }
78 |
79 | return null;
80 | }
81 |
82 | private static NewItemTarget CreateFromProjectItem(ProjectItem projectItem)
83 | {
84 | if (projectItem.IsKind(Constants.vsProjectItemKindSolutionItems))
85 | {
86 | return new NewItemTarget(
87 | GetSolutionFolderPath(projectItem.ContainingProject),
88 | projectItem.ContainingProject,
89 | null,
90 | isSolutionOrSolutionFolder: true);
91 | }
92 | else
93 | {
94 | // The selected item needs a directory. This project item could be
95 | // a virtual folder, so resolve it to a physical file or folder.
96 | var physical_projectItem = ResolveToPhysicalProjectItem(projectItem);
97 | string fileName = physical_projectItem?.GetFileName();
98 |
99 | if (string.IsNullOrEmpty(fileName))
100 | {
101 | return null;
102 | }
103 |
104 | // If the file exists, then it must be a file and we can get the
105 | // directory name from it. If the file does not exist, then it
106 | // must be a directory, and the directory name is the file name.
107 | string directory = File.Exists(fileName) ? Path.GetDirectoryName(fileName) : fileName;
108 | return new NewItemTarget(directory, projectItem.ContainingProject, projectItem, isSolutionOrSolutionFolder: false);
109 | }
110 | }
111 |
112 | private static ProjectItem ResolveToPhysicalProjectItem(ProjectItem projectItem)
113 | {
114 | ThreadHelper.ThrowIfNotOnUIThread();
115 |
116 | if (projectItem.IsKind(Constants.vsProjectItemKindVirtualFolder))
117 | {
118 | // Find the first descendant item that is not a virtual folder.
119 | return projectItem.ProjectItems
120 | .Cast()
121 | .Select(item => ResolveToPhysicalProjectItem(item))
122 | .FirstOrDefault(item => item != null);
123 | }
124 |
125 | return projectItem;
126 | }
127 |
128 | private static string GetSolutionFolderPath(Project folder)
129 | {
130 | ThreadHelper.ThrowIfNotOnUIThread();
131 |
132 | string solutionDirectory = Path.GetDirectoryName(folder.DTE.Solution.FullName);
133 | List segments = new List();
134 |
135 | // Record the names of each folder up the
136 | // hierarchy until we reach the solution.
137 | do
138 | {
139 | segments.Add(folder.Name);
140 | folder = folder.ParentProjectItem?.ContainingProject;
141 | } while (folder != null);
142 |
143 | // Because we walked up the hierarchy,
144 | // the path segments are in reverse order.
145 | segments.Reverse();
146 |
147 | return Path.Combine(new[] { solutionDirectory }.Concat(segments).ToArray());
148 | }
149 |
150 | private NewItemTarget(string directory, Project project, ProjectItem projectItem, bool isSolutionOrSolutionFolder)
151 | {
152 | Directory = directory;
153 | Project = project;
154 | ProjectItem = projectItem;
155 | IsSolutionOrSolutionFolder = isSolutionOrSolutionFolder;
156 | }
157 |
158 | public string Directory { get; }
159 |
160 | public Project Project { get; }
161 |
162 | public ProjectItem ProjectItem { get; }
163 |
164 | public bool IsSolutionOrSolutionFolder { get; }
165 |
166 | public bool IsSolution => IsSolutionOrSolutionFolder && Project == null;
167 |
168 | public bool IsSolutionFolder => IsSolutionOrSolutionFolder && Project != null;
169 |
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/Helpers/VsTheme.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 |
6 | using Microsoft.VisualStudio.PlatformUI;
7 | using Microsoft.VisualStudio.Shell;
8 |
9 | namespace MadsKristensen.AddAnyFile
10 | {
11 | public static class VsTheme
12 | {
13 | private static Dictionary _isUsingVsTheme = new Dictionary();
14 | private static Dictionary _originalBackgrounds = new Dictionary();
15 |
16 | public static DependencyProperty UseVsThemeProperty = DependencyProperty.RegisterAttached("UseVsTheme", typeof(bool), typeof(VsTheme), new PropertyMetadata(false, UseVsThemePropertyChanged));
17 |
18 | private static void UseVsThemePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
19 | {
20 | SetUseVsTheme((UIElement)d, (bool)e.NewValue);
21 | }
22 |
23 | public static void SetUseVsTheme(UIElement element, bool value)
24 | {
25 | if (value)
26 | {
27 | if (!_originalBackgrounds.ContainsKey(element) && element is Control c)
28 | {
29 | _originalBackgrounds[element] = c.Background;
30 | }
31 |
32 | ((ContentControl)element).ShouldBeThemed();
33 | }
34 | else
35 | {
36 | ((ContentControl)element).ShouldNotBeThemed();
37 | }
38 |
39 | _isUsingVsTheme[element] = value;
40 | }
41 |
42 | public static bool GetUseVsTheme(UIElement element)
43 | {
44 | return _isUsingVsTheme.TryGetValue(element, out bool value) && value;
45 | }
46 |
47 | private static ResourceDictionary BuildThemeResources()
48 | {
49 | var allResources = new ResourceDictionary();
50 |
51 | try
52 | {
53 | var shellResources = (ResourceDictionary)Application.LoadComponent(new Uri("Microsoft.VisualStudio.Platform.WindowManagement;component/Themes/ThemedDialogDefaultStyles.xaml", UriKind.Relative));
54 | var scrollStyleContainer = (ResourceDictionary)Application.LoadComponent(new Uri("Microsoft.VisualStudio.Shell.UI.Internal;component/Styles/ScrollBarStyle.xaml", UriKind.Relative));
55 | allResources.MergedDictionaries.Add(shellResources);
56 | allResources.MergedDictionaries.Add(scrollStyleContainer);
57 | allResources[typeof(ScrollViewer)] = new Style
58 | {
59 | TargetType = typeof(ScrollViewer),
60 | BasedOn = (Style)scrollStyleContainer[VsResourceKeys.ScrollViewerStyleKey]
61 | };
62 |
63 | allResources[typeof(TextBox)] = new Style
64 | {
65 | TargetType = typeof(TextBox),
66 | BasedOn = (Style)shellResources[typeof(TextBox)],
67 | Setters =
68 | {
69 | new Setter(Control.PaddingProperty, new Thickness(2, 3, 2, 3))
70 | }
71 | };
72 |
73 | allResources[typeof(ComboBox)] = new Style
74 | {
75 | TargetType = typeof(ComboBox),
76 | BasedOn = (Style)shellResources[typeof(ComboBox)],
77 | Setters =
78 | {
79 | new Setter(Control.PaddingProperty, new Thickness(2, 3, 2, 3))
80 | }
81 | };
82 | }
83 | catch
84 | { }
85 |
86 | return allResources;
87 | }
88 |
89 | private static ResourceDictionary ThemeResources { get; } = BuildThemeResources();
90 |
91 | private static void ShouldBeThemed(this FrameworkElement control)
92 | {
93 | if (control.Resources == null)
94 | {
95 | control.Resources = ThemeResources;
96 | }
97 | else if (control.Resources != ThemeResources)
98 | {
99 | var d = new ResourceDictionary();
100 | d.MergedDictionaries.Add(ThemeResources);
101 | d.MergedDictionaries.Add(control.Resources);
102 | control.Resources = null;
103 | control.Resources = d;
104 | }
105 |
106 | if (control is Control c)
107 | {
108 | c.SetResourceReference(Control.BackgroundProperty, (string)EnvironmentColors.StartPageTabBackgroundBrushKey);
109 | }
110 | }
111 |
112 | private static void ShouldNotBeThemed(this FrameworkElement control)
113 | {
114 | if (control.Resources != null)
115 | {
116 | if (control.Resources == ThemeResources)
117 | {
118 | control.Resources = new ResourceDictionary();
119 | }
120 | else
121 | {
122 | control.Resources.MergedDictionaries.Remove(ThemeResources);
123 | }
124 | }
125 |
126 | //If we're themed now and we're something with a background property, reset it
127 | if (GetUseVsTheme(control) && control is Control c)
128 | {
129 | if (_originalBackgrounds.TryGetValue(control, out object background))
130 | {
131 | c.SetValue(Control.BackgroundProperty, background);
132 | }
133 | else
134 | {
135 | c.ClearValue(Control.BackgroundProperty);
136 | }
137 | }
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/src/Templatemap.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 | using Microsoft.VisualStudio.Shell;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using System.Text.RegularExpressions;
9 | using System.Threading.Tasks;
10 |
11 | namespace MadsKristensen.AddAnyFile
12 | {
13 | internal static class TemplateMap
14 | {
15 | private static readonly string _folder;
16 | private static readonly List _templateFiles = new List();
17 | private const string _defaultExt = ".txt";
18 | private const string _templateDir = ".templates";
19 |
20 | static TemplateMap()
21 | {
22 | var folder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
23 | var userProfile = Path.Combine(folder, ".vs", _templateDir);
24 |
25 | if (Directory.Exists(userProfile))
26 | {
27 | _templateFiles.AddRange(Directory.GetFiles(userProfile, "*" + _defaultExt, SearchOption.AllDirectories));
28 | }
29 |
30 | var assembly = Assembly.GetExecutingAssembly().Location;
31 | _folder = Path.Combine(Path.GetDirectoryName(assembly), "Templates");
32 | _templateFiles.AddRange(Directory.GetFiles(_folder, "*" + _defaultExt, SearchOption.AllDirectories));
33 | }
34 |
35 | public static async Task GetTemplateFilePathAsync(Project project, string file)
36 | {
37 | var name = Path.GetFileName(file);
38 | var safeName = name.StartsWith(".") ? name : Path.GetFileNameWithoutExtension(file);
39 | var relative = PackageUtilities.MakeRelative(project.GetRootFolder(), Path.GetDirectoryName(file) ?? "");
40 |
41 | var list = _templateFiles.ToList();
42 |
43 | AddTemplatesFromCurrentFolder(list, Path.GetDirectoryName(file));
44 |
45 | var templateFile = GetMatchingTemplateFromFileName(project, list, file);
46 |
47 | var template = await ReplaceTokensAsync(project, safeName, relative, templateFile);
48 | return NormalizeLineEndings(template);
49 | }
50 |
51 | private static void AddTemplatesFromCurrentFolder(List list, string dir)
52 | {
53 | var current = new DirectoryInfo(dir);
54 | var dynaList = new List();
55 |
56 | while (current != null)
57 | {
58 | var tmplDir = Path.Combine(current.FullName, _templateDir);
59 |
60 | if (Directory.Exists(tmplDir))
61 | {
62 | dynaList.AddRange(Directory.GetFiles(tmplDir, "*" + _defaultExt, SearchOption.AllDirectories));
63 | }
64 |
65 | current = current.Parent;
66 | }
67 |
68 | list.InsertRange(0, dynaList);
69 | }
70 |
71 | private static string GetMatchingTemplateFromFileName(Project project, List templateFilePaths, string file)
72 | {
73 | var extension = Path.GetExtension(file).ToLowerInvariant();
74 | var name = Path.GetFileName(file);
75 | var safeName = name.StartsWith(".") ? name : Path.GetFileNameWithoutExtension(file);
76 |
77 | // Look for direct file name matches
78 | bool directFileMatchingPredicate(string path) => Path.GetFileName(path).Equals(name + _defaultExt, StringComparison.OrdinalIgnoreCase);
79 | if (templateFilePaths.Any(directFileMatchingPredicate))
80 | {
81 | var tmplFile = templateFilePaths.FirstOrDefault(directFileMatchingPredicate);
82 | return Path.Combine(Path.GetDirectoryName(tmplFile), name + _defaultExt);//GetTemplate(name);
83 | }
84 |
85 | // Look for convention matches
86 | bool conventionMatchingPredicate(string path) => (safeName + _defaultExt).EndsWith(Path.GetFileName(path), StringComparison.OrdinalIgnoreCase);
87 | if (templateFilePaths.Any(conventionMatchingPredicate))
88 | {
89 | return templateFilePaths.FirstOrDefault(conventionMatchingPredicate);
90 | }
91 |
92 | // Look for file extension matches
93 | bool extensionMatchingPredicate(string path) => Path.GetFileName(path).Equals(extension + _defaultExt, StringComparison.OrdinalIgnoreCase) && File.Exists(path);
94 | if (templateFilePaths.Any(extensionMatchingPredicate))
95 | {
96 | var tmplFile = templateFilePaths.FirstOrDefault(extensionMatchingPredicate);
97 | var tmpl = AdjustForSpecific(project, safeName, extension);
98 | return Path.Combine(Path.GetDirectoryName(tmplFile), tmpl + _defaultExt); //GetTemplate(tmpl);
99 | }
100 |
101 | return null;
102 | }
103 |
104 | private static async Task ReplaceTokensAsync(Project project, string name, string relative, string templateFile)
105 | {
106 | if (string.IsNullOrEmpty(templateFile))
107 | {
108 | return templateFile;
109 | }
110 |
111 | var rootNs = project.GetRootNamespace();
112 | var ns = string.IsNullOrEmpty(rootNs) ? "MyNamespace" : rootNs;
113 |
114 | var mvcProjectControllerNs = project.GetMVCNamespace() ?? "";
115 |
116 | if (!string.IsNullOrEmpty(relative))
117 | {
118 | ns += "." + ProjectHelpers.CleanNameSpace(relative);
119 | }
120 |
121 | using (var reader = new StreamReader(templateFile))
122 | {
123 | var content = await reader.ReadToEndAsync();
124 |
125 | return content.Replace("{namespace}", ns)
126 | .Replace("{itemname}", name)
127 | .Replace("{mvcprojectnamespace}", mvcProjectControllerNs);
128 | }
129 | }
130 |
131 | private static string NormalizeLineEndings(string content)
132 | {
133 | if (string.IsNullOrEmpty(content))
134 | {
135 | return content;
136 | }
137 |
138 | return Regex.Replace(content, @"\r\n|\n\r|\n|\r", "\r\n");
139 | }
140 |
141 | private static string AdjustForSpecific(Project project, string safeName, string extension)
142 | {
143 | if (Regex.IsMatch(safeName, "^I[A-Z].*"))
144 | {
145 | return extension += "-interface";
146 | }
147 | else if (Regex.IsMatch(safeName, @".+Enum$"))
148 | {
149 | return extension += "-enum";
150 | }
151 | else if (Regex.IsMatch(safeName, @".+Controller$") && project.IsMVCProject())
152 | {
153 | return extension += "-controller";
154 | }
155 |
156 | return extension;
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Looking to contribute something? **Here's how you can help.**
4 |
5 | Please take a moment to review this document in order to make the contribution
6 | process easy and effective for everyone involved.
7 |
8 | Following these guidelines helps to communicate that you respect the time of
9 | the developers managing and developing this open source project. In return,
10 | they should reciprocate that respect in addressing your issue or assessing
11 | patches and features.
12 |
13 |
14 | ## Using the issue tracker
15 |
16 | The issue tracker is the preferred channel for [bug reports](#bug-reports),
17 | [features requests](#feature-requests) and
18 | [submitting pull requests](#pull-requests), but please respect the
19 | following restrictions:
20 |
21 | * Please **do not** use the issue tracker for personal support requests. Stack
22 | Overflow is a better place to get help.
23 |
24 | * Please **do not** derail or troll issues. Keep the discussion on topic and
25 | respect the opinions of others.
26 |
27 | * Please **do not** open issues or pull requests which *belongs to* third party
28 | components.
29 |
30 |
31 | ## Bug reports
32 |
33 | A bug is a _demonstrable problem_ that is caused by the code in the repository.
34 | Good bug reports are extremely helpful, so thanks!
35 |
36 | Guidelines for bug reports:
37 |
38 | 1. **Use the GitHub issue search** — check if the issue has already been
39 | reported.
40 |
41 | 2. **Check if the issue has been fixed** — try to reproduce it using the
42 | latest `master` or development branch in the repository.
43 |
44 | 3. **Isolate the problem** — ideally create an
45 | [SSCCE](http://www.sscce.org/) and a live example.
46 | Uploading the project on cloud storage (OneDrive, DropBox, et el.)
47 | or creating a sample GitHub repository is also helpful.
48 |
49 |
50 | A good bug report shouldn't leave others needing to chase you up for more
51 | information. Please try to be as detailed as possible in your report. What is
52 | your environment? What steps will reproduce the issue? What browser(s) and OS
53 | experience the problem? Do other browsers show the bug differently? What
54 | would you expect to be the outcome? All these details will help people to fix
55 | any potential bugs.
56 |
57 | Example:
58 |
59 | > Short and descriptive example bug report title
60 | >
61 | > A summary of the issue and the Visual Studio, browser, OS environments
62 | > in which it occurs. If suitable, include the steps required to reproduce the bug.
63 | >
64 | > 1. This is the first step
65 | > 2. This is the second step
66 | > 3. Further steps, etc.
67 | >
68 | > `` - a link to the project/file uploaded on cloud storage or other publicly accessible medium.
69 | >
70 | > Any other information you want to share that is relevant to the issue being
71 | > reported. This might include the lines of code that you have identified as
72 | > causing the bug, and potential solutions (and your opinions on their
73 | > merits).
74 |
75 |
76 | ## Feature requests
77 |
78 | Feature requests are welcome. But take a moment to find out whether your idea
79 | fits with the scope and aims of the project. It's up to *you* to make a strong
80 | case to convince the project's developers of the merits of this feature. Please
81 | provide as much detail and context as possible.
82 |
83 |
84 | ## Pull requests
85 |
86 | Good pull requests, patches, improvements and new features are a fantastic
87 | help. They should remain focused in scope and avoid containing unrelated
88 | commits.
89 |
90 | **Please ask first** before embarking on any significant pull request (e.g.
91 | implementing features, refactoring code, porting to a different language),
92 | otherwise you risk spending a lot of time working on something that the
93 | project's developers might not want to merge into the project.
94 |
95 | Please adhere to the [coding guidelines](#code-guidelines) used throughout the
96 | project (indentation, accurate comments, etc.) and any other requirements
97 | (such as test coverage).
98 |
99 | Adhering to the following process is the best way to get your work
100 | included in the project:
101 |
102 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
103 | and configure the remotes:
104 |
105 | ```bash
106 | # Clone your fork of the repo into the current directory
107 | git clone https://github.com//.git
108 | # Navigate to the newly cloned directory
109 | cd
110 | # Assign the original repo to a remote called "upstream"
111 | git remote add upstream https://github.com/madskristensen/.git
112 | ```
113 |
114 | 2. If you cloned a while ago, get the latest changes from upstream:
115 |
116 | ```bash
117 | git checkout master
118 | git pull upstream master
119 | ```
120 |
121 | 3. Create a new topic branch (off the main project development branch) to
122 | contain your feature, change, or fix:
123 |
124 | ```bash
125 | git checkout -b
126 | ```
127 |
128 | 4. Commit your changes in logical chunks. Please adhere to these [git commit
129 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
130 | or your code is unlikely be merged into the main project. Use Git's
131 | [interactive rebase](https://help.github.com/articles/interactive-rebase)
132 | feature to tidy up your commits before making them public. Also, prepend name of the feature
133 | to the commit message. For instance: "SCSS: Fixes compiler results for IFileListener.\nFixes `#123`"
134 |
135 | 5. Locally merge (or rebase) the upstream development branch into your topic branch:
136 |
137 | ```bash
138 | git pull [--rebase] upstream master
139 | ```
140 |
141 | 6. Push your topic branch up to your fork:
142 |
143 | ```bash
144 | git push origin
145 | ```
146 |
147 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
148 | with a clear title and description against the `master` branch.
149 |
150 |
151 | ## Code guidelines
152 |
153 | - Always use proper indentation.
154 | - In Visual Studio under `Tools > Options > Text Editor > C# > Advanced`, make sure
155 | `Place 'System' directives first when sorting usings` option is enabled (checked).
156 | - Before committing, organize usings for each updated C# source file. Either you can
157 | right-click editor and select `Organize Usings > Remove and sort` OR use extension
158 | like [BatchFormat](https://marketplace.visualstudio.com/items?itemName=vs-publisher-147549.BatchFormat).
159 | - Before committing, run Code Analysis in `Debug` configuration and follow the guidelines
160 | to fix CA issues. Code Analysis commits can be made separately.
161 |
--------------------------------------------------------------------------------
/src/AddAnyFile.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(VisualStudioVersion)
5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
6 | Program
7 | $(DevEnvDir)\devenv.exe
8 | /rootsuffix Exp
9 | true
10 | publish\
11 | true
12 | Disk
13 | false
14 | Foreground
15 | 7
16 | Days
17 | false
18 | false
19 | true
20 | 0
21 | 1.0.0.%2a
22 | false
23 | false
24 | true
25 |
26 |
27 |
28 |
29 |
30 | bin\CI\
31 | TRACE
32 | true
33 | pdbonly
34 | AnyCPU
35 | false
36 | prompt
37 | MinimumRecommendedRules.ruleset
38 | False
39 |
40 |
41 |
42 | Debug
43 | AnyCPU
44 | 2.0
45 | {CAD947D3-06E2-4A76-8838-68115036B179}
46 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
47 | Library
48 | Properties
49 | MadsKristensen.AddAnyFile
50 | AddAnyFile
51 | v4.7.2
52 |
53 |
54 | true
55 | full
56 | false
57 | bin\Debug\
58 | DEBUG;TRACE
59 | prompt
60 | 4
61 |
62 |
63 | pdbonly
64 | true
65 | bin\Release\
66 | TRACE
67 | prompt
68 | 4
69 | false
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | True
87 | True
88 | AddAnyFile.vsct
89 |
90 |
91 | FileNameDialog.xaml
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | True
101 | True
102 | source.extension.vsixmanifest
103 |
104 |
105 |
106 | true
107 |
108 |
109 | true
110 |
111 |
112 | true
113 |
114 |
115 | true
116 |
117 |
118 | true
119 |
120 |
121 | true
122 |
123 |
124 | true
125 |
126 |
127 | true
128 |
129 |
130 | true
131 |
132 |
133 | true
134 |
135 |
136 | true
137 |
138 |
139 | true
140 |
141 |
142 | true
143 |
144 |
145 |
146 |
147 |
148 | Resources\LICENSE
149 | true
150 |
151 |
152 | Designer
153 | VsixManifestGenerator
154 | source.extension.cs
155 |
156 |
157 | true
158 |
159 |
160 | true
161 |
162 |
163 |
164 |
165 | Menus.ctmenu
166 | Designer
167 | VsctGenerator
168 | AddAnyFile.cs
169 |
170 |
171 |
172 |
173 | true
174 |
175 |
176 |
177 |
178 | Designer
179 | MSBuild:Compile
180 |
181 |
182 |
183 |
184 | False
185 | Microsoft .NET Framework 4.5 %28x86 and x64%29
186 | true
187 |
188 |
189 | False
190 | .NET Framework 3.5 SP1
191 | false
192 |
193 |
194 |
195 |
196 | 17.10.40171
197 | compile; build; native; contentfiles; analyzers; buildtransitive
198 |
199 |
200 | 17.11.414
201 | runtime; build; native; contentfiles; analyzers; buildtransitive
202 | all
203 |
204 |
205 |
206 | true
207 |
208 |
209 |
210 |
217 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Mads Kristensen
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/Helpers/ProjectHelpers.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 | using EnvDTE80;
3 |
4 | using Microsoft;
5 | using Microsoft.VisualStudio;
6 | using Microsoft.VisualStudio.ComponentModelHost;
7 | using Microsoft.VisualStudio.Editor;
8 | using Microsoft.VisualStudio.Shell;
9 | using Microsoft.VisualStudio.Shell.Interop;
10 | using Microsoft.VisualStudio.Text.Editor;
11 | using Microsoft.VisualStudio.TextManager.Interop;
12 |
13 | using System;
14 | using System.Collections.Generic;
15 | using System.IO;
16 | using System.Linq;
17 | using System.Runtime.InteropServices;
18 | using VSLangProj;
19 |
20 | namespace MadsKristensen.AddAnyFile
21 | {
22 | public static class ProjectHelpers
23 | {
24 | private static readonly DTE2 _dte = AddAnyFilePackage._dte;
25 |
26 | public static string GetRootNamespace(this Project project)
27 | {
28 | if (project == null)
29 | {
30 | return null;
31 | }
32 |
33 | string ns = project.Name ?? string.Empty;
34 |
35 | try
36 | {
37 | Property prop = project.Properties.Item("RootNamespace");
38 |
39 | if (prop != null && prop.Value != null && !string.IsNullOrEmpty(prop.Value.ToString()))
40 | {
41 | ns = prop.Value.ToString();
42 | }
43 | }
44 | catch { /* Project doesn't have a root namespace */ }
45 |
46 | return CleanNameSpace(ns, stripPeriods: false);
47 | }
48 |
49 | public static string CleanNameSpace(string ns, bool stripPeriods = true)
50 | {
51 | if (stripPeriods)
52 | {
53 | ns = ns.Replace(".", "");
54 | }
55 |
56 | ns = ns.Replace(" ", "")
57 | .Replace("-", "")
58 | .Replace("\\", ".");
59 |
60 | return ns;
61 | }
62 |
63 | public static string GetRootFolder(this Project project)
64 | {
65 | if (project == null)
66 | {
67 | return null;
68 | }
69 |
70 | if (project.IsKind("{66A26720-8FB5-11D2-AA7E-00C04F688DDE}")) //ProjectKinds.vsProjectKindSolutionFolder
71 | {
72 | return Path.GetDirectoryName(_dte.Solution.FullName);
73 | }
74 |
75 | if (string.IsNullOrEmpty(project.FullName))
76 | {
77 | return null;
78 | }
79 |
80 | string fullPath;
81 |
82 | try
83 | {
84 | fullPath = project.Properties.Item("FullPath").Value as string;
85 | }
86 | catch (ArgumentException)
87 | {
88 | try
89 | {
90 | // MFC projects don't have FullPath, and there seems to be no way to query existence
91 | fullPath = project.Properties.Item("ProjectDirectory").Value as string;
92 | }
93 | catch (ArgumentException)
94 | {
95 | // Installer projects have a ProjectPath.
96 | fullPath = project.Properties.Item("ProjectPath").Value as string;
97 | }
98 | }
99 |
100 | if (string.IsNullOrEmpty(fullPath))
101 | {
102 | return File.Exists(project.FullName) ? Path.GetDirectoryName(project.FullName) : null;
103 | }
104 |
105 | if (Directory.Exists(fullPath))
106 | {
107 | return fullPath;
108 | }
109 |
110 | if (File.Exists(fullPath))
111 | {
112 | return Path.GetDirectoryName(fullPath);
113 | }
114 |
115 | return null;
116 | }
117 |
118 | public static ProjectItem AddFileToProject(this Project project, FileInfo file, string itemType = null)
119 | {
120 | ThreadHelper.ThrowIfNotOnUIThread();
121 | if (project.IsKind(ProjectTypes.ASPNET_5, ProjectTypes.SSDT))
122 | {
123 | return _dte.Solution.FindProjectItem(file.FullName);
124 | }
125 |
126 | string root = project.GetRootFolder();
127 |
128 | if (string.IsNullOrEmpty(root) || !file.FullName.StartsWith(root, StringComparison.OrdinalIgnoreCase))
129 | {
130 | return null;
131 | }
132 |
133 | // Appears to be a bug in project system or VS, so need to make sure the project is aware of the folder structure first,
134 | // then add the time to that folderStructure (create and/or find)
135 | // if adding to the root ProjectItem then just do that.
136 | ProjectItems projectItems;
137 | if (string.Equals(root.TrimEnd(Path.DirectorySeparatorChar), file.DirectoryName, StringComparison.InvariantCultureIgnoreCase))
138 | {
139 | projectItems = project.ProjectItems;
140 | }
141 | else
142 | {
143 | projectItems = AddFolders(project, file.DirectoryName).ProjectItems;
144 | }
145 |
146 | ProjectItem item = projectItems.AddFromTemplate(file.FullName, file.Name);
147 | item.SetItemType(itemType);
148 |
149 | return item;
150 | }
151 |
152 | public static ProjectItem AddFolders(Project project, string targetFolder)
153 | {
154 | ThreadHelper.ThrowIfNotOnUIThread();
155 |
156 | List list = new List();
157 | DirectoryInfo root = new DirectoryInfo(project.GetRootFolder());
158 | DirectoryInfo target = new DirectoryInfo(targetFolder);
159 |
160 | while (!target.FullName.Equals(root.FullName.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
161 | {
162 | list.Add(target.Name);
163 | target = target.Parent;
164 | }
165 |
166 | list.Reverse();
167 |
168 | ProjectItem existing = project.ProjectItems.Cast().FirstOrDefault(i => i.Name.Equals(list.First(), StringComparison.OrdinalIgnoreCase));
169 | ProjectItem item = existing ?? project.ProjectItems.AddFolder(list.First());
170 |
171 | foreach (string folder in list.Skip(1))
172 | {
173 | existing = item.ProjectItems.Cast().FirstOrDefault(i => i.Name.Equals(folder, StringComparison.OrdinalIgnoreCase));
174 | item = existing ?? item.ProjectItems.AddFolder(folder);
175 | }
176 |
177 | return item;
178 | }
179 |
180 | public static void SetItemType(this ProjectItem item, string itemType)
181 | {
182 | try
183 | {
184 | if (item == null || item.ContainingProject == null)
185 | {
186 | return;
187 | }
188 |
189 | if (string.IsNullOrEmpty(itemType)
190 | || item.ContainingProject.IsKind(ProjectTypes.WEBSITE_PROJECT)
191 | || item.ContainingProject.IsKind(ProjectTypes.UNIVERSAL_APP))
192 | {
193 | return;
194 | }
195 |
196 | item.Properties.Item("ItemType").Value = itemType;
197 | }
198 | catch (Exception ex)
199 | {
200 | Logger.Log(ex);
201 | }
202 | }
203 |
204 | public static string GetFileName(this ProjectItem item)
205 | {
206 | try
207 | {
208 | return item?.Properties?.Item("FullPath").Value?.ToString();
209 | }
210 | catch (ArgumentException)
211 | {
212 | // The property does not exist.
213 | return null;
214 | }
215 | }
216 |
217 | public static Project FindSolutionFolder(this Solution solution, string name)
218 | {
219 | return solution.Projects.OfType()
220 | .Where(p => p.IsKind(EnvDTE.Constants.vsProjectKindSolutionItems))
221 | .Where(p => p.Name == name)
222 | .FirstOrDefault();
223 | }
224 |
225 | public static Project FindSolutionFolder(this Project project, string name)
226 | {
227 | return project.ProjectItems.OfType()
228 | .Where(p => p.IsKind(EnvDTE.Constants.vsProjectItemKindSolutionItems))
229 | .Where(p => p.Name == name)
230 | .Select(p => p.SubProject)
231 | .FirstOrDefault();
232 | }
233 |
234 | public static bool IsKind(this Project project, params string[] kindGuids)
235 | {
236 | foreach (string guid in kindGuids)
237 | {
238 | if (project.Kind.Equals(guid, StringComparison.OrdinalIgnoreCase))
239 | {
240 | return true;
241 | }
242 | }
243 |
244 | return false;
245 | }
246 |
247 | public static bool IsKind(this ProjectItem projectItem, params string[] kindGuids)
248 | {
249 | foreach (string guid in kindGuids)
250 | {
251 | if (projectItem.Kind.Equals(guid, StringComparison.OrdinalIgnoreCase))
252 | {
253 | return true;
254 | }
255 | }
256 |
257 | return false;
258 | }
259 |
260 | public static string GetMVCNamespace(this Project project)
261 | {
262 | try
263 | {
264 | ThreadHelper.ThrowIfNotOnUIThread();
265 | var reference = (project.Object as VSProject).References.Cast().Where(x => x.Name == "Microsoft.AspNetCore.Mvc" || x.Name == "System.Web.Mvc").FirstOrDefault();
266 | return reference.Name;
267 | }
268 | catch
269 | {
270 | return null;
271 | }
272 | }
273 |
274 | public static bool IsMVCProject(this Project project)
275 | {
276 | return project.GetMVCNamespace() != null;
277 | }
278 |
279 | private static IEnumerable GetChildProjects(Project parent)
280 | {
281 | try
282 | {
283 | if (!parent.IsKind("{66A26720-8FB5-11D2-AA7E-00C04F688DDE}") && parent.Collection == null) // Unloaded
284 | {
285 | return Enumerable.Empty();
286 | }
287 |
288 | if (!string.IsNullOrEmpty(parent.FullName))
289 | {
290 | return new[] { parent };
291 | }
292 | }
293 | catch (COMException)
294 | {
295 | return Enumerable.Empty();
296 | }
297 |
298 | return parent.ProjectItems
299 | .Cast()
300 | .Where(p => p.SubProject != null)
301 | .SelectMany(p => GetChildProjects(p.SubProject));
302 | }
303 |
304 | public static Project GetActiveProject()
305 | {
306 | try
307 | {
308 |
309 | if (_dte.ActiveSolutionProjects is Array activeSolutionProjects && activeSolutionProjects.Length > 0)
310 | {
311 | return activeSolutionProjects.GetValue(0) as Project;
312 | }
313 |
314 | Document doc = _dte.ActiveDocument;
315 |
316 | if (doc != null && !string.IsNullOrEmpty(doc.FullName))
317 | {
318 | ProjectItem item = _dte.Solution?.FindProjectItem(doc.FullName);
319 |
320 | if (item != null)
321 | {
322 | return item.ContainingProject;
323 | }
324 | }
325 | }
326 | catch (Exception ex)
327 | {
328 | Logger.Log("Error getting the active project" + ex);
329 | }
330 |
331 | return null;
332 | }
333 |
334 | public static IWpfTextView GetCurentTextView()
335 | {
336 | IComponentModel componentModel = GetComponentModel();
337 | if (componentModel == null)
338 | {
339 | return null;
340 | }
341 |
342 | IVsEditorAdaptersFactoryService editorAdapter = componentModel.GetService();
343 |
344 | return editorAdapter.GetWpfTextView(GetCurrentNativeTextView());
345 | }
346 |
347 | public static IVsTextView GetCurrentNativeTextView()
348 | {
349 | IVsTextManager textManager = (IVsTextManager)ServiceProvider.GlobalProvider.GetService(typeof(SVsTextManager));
350 | Assumes.Present(textManager);
351 |
352 | ErrorHandler.ThrowOnFailure(textManager.GetActiveView(1, null, out IVsTextView activeView));
353 | return activeView;
354 | }
355 |
356 | public static IComponentModel GetComponentModel()
357 | {
358 | return (IComponentModel)AddAnyFilePackage.GetGlobalService(typeof(SComponentModel));
359 | }
360 |
361 | public static object GetSelectedItem()
362 | {
363 | object selectedObject = null;
364 |
365 | IVsMonitorSelection monitorSelection = (IVsMonitorSelection)Package.GetGlobalService(typeof(SVsShellMonitorSelection));
366 |
367 | try
368 | {
369 | monitorSelection.GetCurrentSelection(out IntPtr hierarchyPointer,
370 | out uint itemId,
371 | out IVsMultiItemSelect multiItemSelect,
372 | out IntPtr selectionContainerPointer);
373 |
374 |
375 | if (Marshal.GetTypedObjectForIUnknown(
376 | hierarchyPointer,
377 | typeof(IVsHierarchy)) is IVsHierarchy selectedHierarchy)
378 | {
379 | ErrorHandler.ThrowOnFailure(selectedHierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_ExtObject, out selectedObject));
380 | }
381 |
382 | Marshal.Release(hierarchyPointer);
383 | Marshal.Release(selectionContainerPointer);
384 | }
385 | catch (Exception ex)
386 | {
387 | System.Diagnostics.Debug.Write(ex);
388 | }
389 |
390 | return selectedObject;
391 | }
392 | }
393 |
394 | public static class ProjectTypes
395 | {
396 | public const string ASPNET_5 = "{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}";
397 | public const string DOTNET_Core = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}";
398 | public const string WEBSITE_PROJECT = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}";
399 | public const string UNIVERSAL_APP = "{262852C6-CD72-467D-83FE-5EEB1973A190}";
400 | public const string NODE_JS = "{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}";
401 | public const string SSDT = "{00d1a9c2-b5f0-4af3-8072-f6c62b433612}";
402 | }
403 | }
404 |
--------------------------------------------------------------------------------
/src/AddAnyFilePackage.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 |
3 | using EnvDTE80;
4 |
5 | using Microsoft;
6 | using Microsoft.VisualStudio;
7 | using Microsoft.VisualStudio.Shell;
8 | using Microsoft.VisualStudio.Text;
9 | using Microsoft.VisualStudio.Threading;
10 | using System;
11 | using System.Collections.Generic;
12 | using System.ComponentModel.Design;
13 | using System.IO;
14 | using System.Linq;
15 | using System.Runtime.InteropServices;
16 | using System.Text;
17 | using System.Text.RegularExpressions;
18 | using System.Threading;
19 | using System.Threading.Tasks;
20 | using System.Windows;
21 |
22 | namespace MadsKristensen.AddAnyFile
23 | {
24 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
25 | [InstalledProductRegistration("#110", "#112", Vsix.Version, IconResourceID = 400)]
26 | [ProvideMenuResource("Menus.ctmenu", 1)]
27 | [Guid(PackageGuids.guidAddAnyFilePkgString)]
28 | public sealed class AddAnyFilePackage : AsyncPackage
29 | {
30 | private const string _solutionItemsProjectName = "Solution Items";
31 | private static readonly Regex _reservedFileNamePattern = new Regex($@"(?i)^(PRN|AUX|NUL|CON|COM\d|LPT\d)(\.|$)");
32 | private static readonly HashSet _invalidFileNameChars = new HashSet(Path.GetInvalidFileNameChars());
33 |
34 | public static DTE2 _dte;
35 |
36 | protected override async System.Threading.Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
37 | {
38 | await JoinableTaskFactory.SwitchToMainThreadAsync();
39 |
40 | _dte = await GetServiceAsync(typeof(DTE)) as DTE2;
41 | Assumes.Present(_dte);
42 |
43 | Logger.Initialize(this, Vsix.Name);
44 |
45 | if (await GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService mcs)
46 | {
47 | CommandID menuCommandID = new CommandID(PackageGuids.guidAddAnyFileCmdSet, PackageIds.cmdidMyCommand);
48 | OleMenuCommand menuItem = new OleMenuCommand(ExecuteAsync, menuCommandID);
49 | mcs.AddCommand(menuItem);
50 | }
51 | }
52 |
53 | private void ExecuteAsync(object sender, EventArgs e)
54 | {
55 | NewItemTarget target = NewItemTarget.Create(_dte);
56 |
57 | if (target == null)
58 | {
59 | MessageBox.Show(
60 | "Could not determine where to create the new file. Select a file or folder in Solution Explorer and try again.",
61 | Vsix.Name,
62 | MessageBoxButton.OK,
63 | MessageBoxImage.Error);
64 | return;
65 | }
66 |
67 | string input = PromptForFileName(target.Directory).TrimStart('/', '\\').Replace("/", "\\");
68 |
69 | if (string.IsNullOrEmpty(input))
70 | {
71 | return;
72 | }
73 |
74 | string[] parsedInputs = GetParsedInput(input);
75 |
76 | foreach (string name in parsedInputs)
77 | {
78 | try
79 | {
80 | AddItemAsync(name, target).Forget();
81 | }
82 | catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex))
83 | {
84 | Logger.Log(ex);
85 | MessageBox.Show(
86 | $"Error creating file '{name}':{Environment.NewLine}{ex.Message}",
87 | Vsix.Name,
88 | MessageBoxButton.OK,
89 | MessageBoxImage.Error);
90 | }
91 | }
92 | }
93 |
94 | private async System.Threading.Tasks.Task AddItemAsync(string name, NewItemTarget target)
95 | {
96 | // The naming rules that apply to files created on disk also apply to virtual solution folders,
97 | // so regardless of what type of item we are creating, we need to validate the name.
98 | ValidatePath(name);
99 |
100 | if (name.EndsWith("\\", StringComparison.Ordinal))
101 | {
102 | if (target.IsSolutionOrSolutionFolder)
103 | {
104 | GetOrAddSolutionFolder(name, target);
105 | }
106 | else
107 | {
108 | AddProjectFolder(name, target);
109 | }
110 | }
111 | else
112 | {
113 | await AddFileAsync(name, target);
114 | }
115 | }
116 |
117 | private void ValidatePath(string path)
118 | {
119 | do
120 | {
121 | string name = Path.GetFileName(path);
122 |
123 | if (_reservedFileNamePattern.IsMatch(name))
124 | {
125 | throw new InvalidOperationException($"The name '{name}' is a system reserved name.");
126 | }
127 |
128 | if (name.Any(c => _invalidFileNameChars.Contains(c)))
129 | {
130 | throw new InvalidOperationException($"The name '{name}' contains invalid characters.");
131 | }
132 |
133 | path = Path.GetDirectoryName(path);
134 | } while (!string.IsNullOrEmpty(path));
135 | }
136 |
137 | private async System.Threading.Tasks.Task AddFileAsync(string name, NewItemTarget target)
138 | {
139 | await JoinableTaskFactory.SwitchToMainThreadAsync();
140 | FileInfo file;
141 |
142 | // If the file is being added to a solution folder, but that
143 | // solution folder doesn't have a corresponding directory on
144 | // disk, then write the file to the root of the solution instead.
145 | if (target.IsSolutionFolder && !Directory.Exists(target.Directory))
146 | {
147 | file = new FileInfo(Path.Combine(Path.GetDirectoryName(_dte.Solution.FullName), Path.GetFileName(name)));
148 | }
149 | else if (name.StartsWith("sln\\"))
150 | {
151 | file = new FileInfo(Path.Combine(Path.GetDirectoryName(_dte.Solution.FullName), Path.GetFileName(name.Substring(4))));
152 | }
153 | else if (name.StartsWith("prj\\") && target.Project != null)
154 | {
155 | file = new FileInfo(Path.Combine(Path.GetDirectoryName(target.Project.FileName), Path.GetFileName(name.Substring(4))));
156 | }
157 | else
158 | {
159 | file = new FileInfo(Path.Combine(target.Directory, name));
160 | }
161 |
162 | // Make sure the directory exists before we create the file. Don't use
163 | // `PackageUtilities.EnsureOutputPath()` because it can silently fail.
164 | Directory.CreateDirectory(file.DirectoryName);
165 |
166 | if (!file.Exists)
167 | {
168 | Project project;
169 |
170 | if (target.IsSolutionOrSolutionFolder)
171 | {
172 | project = GetOrAddSolutionFolder(Path.GetDirectoryName(name), target);
173 | }
174 | else
175 | {
176 | project = target.Project;
177 | }
178 |
179 | int position = await WriteFileAsync(project, file.FullName);
180 | if (target.ProjectItem != null && target.ProjectItem.IsKind(Constants.vsProjectItemKindVirtualFolder))
181 | {
182 | target.ProjectItem.ProjectItems.AddFromFile(file.FullName);
183 | }
184 | else
185 | {
186 | project.AddFileToProject(file);
187 | }
188 |
189 | VsShellUtilities.OpenDocument(this, file.FullName);
190 |
191 | // Move cursor into position.
192 | if (position > 0)
193 | {
194 | Microsoft.VisualStudio.Text.Editor.IWpfTextView view = ProjectHelpers.GetCurentTextView();
195 |
196 | if (view != null)
197 | {
198 | view.Caret.MoveTo(new SnapshotPoint(view.TextBuffer.CurrentSnapshot, position));
199 | }
200 | }
201 |
202 | ExecuteCommandIfAvailable("SolutionExplorer.SyncWithActiveDocument");
203 | _dte.ActiveDocument.Activate();
204 | }
205 | else
206 | {
207 | MessageBox.Show($"The file '{file}' already exists.", Vsix.Name, MessageBoxButton.OK, MessageBoxImage.Information);
208 | }
209 | }
210 |
211 | private static async Task WriteFileAsync(Project project, string file)
212 | {
213 | string template = await TemplateMap.GetTemplateFilePathAsync(project, file);
214 |
215 | if (!string.IsNullOrEmpty(template))
216 | {
217 | int index = template.IndexOf('$');
218 |
219 | if (index > -1)
220 | {
221 | template = template.Remove(index, 1);
222 | }
223 |
224 | await WriteToDiskAsync(file, template);
225 | return index;
226 | }
227 |
228 | await WriteToDiskAsync(file, string.Empty);
229 |
230 | return 0;
231 | }
232 |
233 | private static async System.Threading.Tasks.Task WriteToDiskAsync(string file, string content)
234 | {
235 | using (StreamWriter writer = new StreamWriter(file, false, GetFileEncoding(file)))
236 | {
237 | await writer.WriteAsync(content);
238 | }
239 | }
240 |
241 | private static Encoding GetFileEncoding(string file)
242 | {
243 | string[] noBom = { ".cmd", ".bat", ".json" };
244 | string ext = Path.GetExtension(file).ToLowerInvariant();
245 |
246 | if (noBom.Contains(ext))
247 | {
248 | return new UTF8Encoding(false);
249 | }
250 |
251 | return new UTF8Encoding(true);
252 | }
253 |
254 | private Project GetOrAddSolutionFolder(string name, NewItemTarget target)
255 | {
256 | if (target.IsSolution && string.IsNullOrEmpty(name))
257 | {
258 | // An empty solution folder name means we are not creating any solution
259 | // folders for that item, and the file we are adding is intended to be
260 | // added to the solution. Files cannot be added directly to the solution,
261 | // so there is a "Solution Items" folder that they are added to.
262 | return _dte.Solution.FindSolutionFolder(_solutionItemsProjectName)
263 | ?? ((Solution2)_dte.Solution).AddSolutionFolder(_solutionItemsProjectName);
264 | }
265 |
266 | // Even though solution folders are always virtual, if the target directory exists,
267 | // then we will also create the new directory on disk. This ensures that any files
268 | // that are added to this folder will end up in the corresponding physical directory.
269 | if (Directory.Exists(target.Directory))
270 | {
271 | // Don't use `PackageUtilities.EnsureOutputPath()` because it can silently fail.
272 | Directory.CreateDirectory(Path.Combine(target.Directory, name));
273 | }
274 |
275 | Project parent = target.Project;
276 |
277 | foreach (string segment in SplitPath(name))
278 | {
279 | // If we don't have a parent project yet,
280 | // then this folder is added to the solution.
281 | if (parent == null)
282 | {
283 | parent = _dte.Solution.FindSolutionFolder(segment) ?? ((Solution2)_dte.Solution).AddSolutionFolder(segment);
284 | }
285 | else
286 | {
287 | parent = parent.FindSolutionFolder(segment) ?? ((SolutionFolder)parent.Object).AddSolutionFolder(segment);
288 | }
289 | }
290 |
291 | return parent;
292 | }
293 |
294 | private void AddProjectFolder(string name, NewItemTarget target)
295 | {
296 | ThreadHelper.ThrowIfNotOnUIThread();
297 |
298 | // Make sure the directory exists before we add it to the project. Don't
299 | // use `PackageUtilities.EnsureOutputPath()` because it can silently fail.
300 | string targetFolder = Path.Combine(target.Directory, name);
301 | Directory.CreateDirectory(targetFolder);
302 | ProjectHelpers.AddFolders(target.Project, targetFolder);
303 | }
304 |
305 | private static string[] SplitPath(string path)
306 | {
307 | return path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
308 | }
309 |
310 | private static string[] GetParsedInput(string input)
311 | {
312 | // var tests = new string[] { "file1.txt", "file1.txt, file2.txt", ".ignore", ".ignore.(old,new)", "license", "folder/",
313 | // "folder\\", "folder\\file.txt", "folder/.thing", "page.aspx.cs", "widget-1.(html,js)", "pages\\home.(aspx, aspx.cs)",
314 | // "home.(html,js), about.(html,js,css)", "backup.2016.(old, new)", "file.(txt,txt,,)", "file_@#d+|%.3-2...3^&.txt" };
315 | Regex pattern = new Regex(@"[,]?([^(,]*)([\.\/\\]?)[(]?((?<=[^(])[^,]*|[^)]+)[)]?");
316 | List results = new List();
317 | Match match = pattern.Match(input);
318 |
319 | while (match.Success)
320 | {
321 | // Always 4 matches w. Group[3] being the extension, extension list, folder terminator ("/" or "\"), or empty string
322 | string path = match.Groups[1].Value.Trim() + match.Groups[2].Value;
323 | string[] extensions = match.Groups[3].Value.Split(',');
324 |
325 | foreach (string ext in extensions)
326 | {
327 | string value = path + ext.Trim();
328 |
329 | // ensure "file.(txt,,txt)" or "file.txt,,file.txt,File.TXT" returns as just ["file.txt"]
330 | if (value != "" && !value.EndsWith(".", StringComparison.Ordinal) && !results.Contains(value, StringComparer.OrdinalIgnoreCase))
331 | {
332 | results.Add(value);
333 | }
334 | }
335 | match = match.NextMatch();
336 | }
337 | return results.ToArray();
338 | }
339 |
340 | private string PromptForFileName(string folder)
341 | {
342 | DirectoryInfo dir = new DirectoryInfo(folder);
343 | FileNameDialog dialog = new FileNameDialog(dir.Name)
344 | {
345 | //IntPtr hwnd = new IntPtr(_dte.MainWindow.HWnd);
346 | //System.Windows.Window window = (System.Windows.Window)HwndSource.FromHwnd(hwnd).RootVisual;
347 | Owner = Application.Current.MainWindow
348 | };
349 |
350 | bool? result = dialog.ShowDialog();
351 | return (result.HasValue && result.Value) ? dialog.Input : string.Empty;
352 | }
353 |
354 | private void ExecuteCommandIfAvailable(string commandName)
355 | {
356 | ThreadHelper.ThrowIfNotOnUIThread();
357 | Command command;
358 |
359 | try
360 | {
361 | command = _dte.Commands.Item(commandName);
362 | }
363 | catch (ArgumentException)
364 | {
365 | // The command does not exist, so we can't execute it.
366 | return;
367 | }
368 |
369 | if (command.IsAvailable)
370 | {
371 | _dte.ExecuteCommand(commandName);
372 | }
373 | }
374 | }
375 | }
--------------------------------------------------------------------------------