diff --git a/VSFormatOnSaveShared/AllowDenyDocumentFilter.cs b/VSFormatOnSaveShared/AllowDenyDocumentFilter.cs index cbeb67d..7273504 100644 --- a/VSFormatOnSaveShared/AllowDenyDocumentFilter.cs +++ b/VSFormatOnSaveShared/AllowDenyDocumentFilter.cs @@ -2,28 +2,100 @@ // See https://github.com/Elders/VSE-FormatDocumentOnSave. using System; +using System.IO; using System.Linq; namespace Tinyfish.FormatOnSave { + public class AllowDenyPathFilter + { + private string[] _whitelistedPaths { get; set; } + private string[] _blacklistedPaths { get; set; } + public AllowDenyPathFilter(string[] whitelistPaths, string[] blacklistPaths) + { + _whitelistedPaths = whitelistPaths.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + _blacklistedPaths = blacklistPaths.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + } + + // allowed if a path is whitelisted or not blacklisted + public bool IsAllowed(string path) + { + if (string.IsNullOrEmpty(path)) + return true; + + bool whiteListed = _whitelistedPaths.Any(x => IsUnderFolder(x, path)); + if (whiteListed) + { + return true; + } + else + { + return !_blacklistedPaths.Any(x => IsUnderFolder(x, path)); + } + } + + private bool CheckIfParentDir(DirectoryInfo childDir, DirectoryInfo parentDir) + { + if (!parentDir.Exists) + return false; + + bool isParent = false; + while (childDir.Parent != null) + { + if (childDir.Parent.FullName == parentDir.FullName) + { + isParent = true; + break; + } + else + { + childDir = childDir.Parent; + } + } + return isParent; + } + + private bool IsUnderFolder(string parent, string child) + { + try + { + DirectoryInfo parentDir = new DirectoryInfo(parent.TrimEnd(Path.DirectorySeparatorChar)); + DirectoryInfo childDir = new DirectoryInfo(child.TrimEnd(Path.DirectorySeparatorChar)); + + if (parentDir.FullName == childDir.FullName) + { + return true; + } + + return CheckIfParentDir(childDir, parentDir); + } + catch + { + return false; + } + } + } + public class AllowDenyDocumentFilter { - private readonly Func _isAllowed = fileName => true; + private readonly Func _isAllowed = (fileName, path, pathFilter) => pathFilter.IsAllowed(path); + private AllowDenyPathFilter _pathFilter; - public AllowDenyDocumentFilter(string[] allowedExtensions, string[] deniedExtensions) + public AllowDenyDocumentFilter(string[] allowedExtensions, string[] deniedExtensions, AllowDenyPathFilter pathfilter) { allowedExtensions = allowedExtensions?.Where(x => !x.Equals(".*") && !string.IsNullOrEmpty(x)).ToArray(); deniedExtensions = deniedExtensions?.Where(x => !x.Equals(".*") && !string.IsNullOrEmpty(x)).ToArray(); + _pathFilter = pathfilter; if (allowedExtensions != null && allowedExtensions.Any()) - _isAllowed = fileName => allowedExtensions.Any(ext => fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); + _isAllowed = (fileName, path, _pathFilter) => allowedExtensions.Any(ext => fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) && _pathFilter.IsAllowed(path); else if (deniedExtensions != null && deniedExtensions.Any()) - _isAllowed = fileName => !deniedExtensions.Any(ext => fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); + _isAllowed = (fileName, path, _pathFilter) => !deniedExtensions.Any(ext => fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) && _pathFilter.IsAllowed(path); } - public bool IsAllowed(string fileName) + public bool IsAllowed(string fileName, string path) { - return _isAllowed(fileName); + return _isAllowed(fileName, path, _pathFilter); } } } diff --git a/VSFormatOnSaveShared/FormatOnSavePackage.cs b/VSFormatOnSaveShared/FormatOnSavePackage.cs index 42de9e8..c2f4a75 100644 --- a/VSFormatOnSaveShared/FormatOnSavePackage.cs +++ b/VSFormatOnSaveShared/FormatOnSavePackage.cs @@ -197,32 +197,32 @@ public bool Format(Document document) vsTextView.SetCaretPos(oldCaretLine, 0); // Do TabToSpace before FormatDocument, since VS format may break the tab formatting. - if (OptionsPage.EnableTabToSpace && OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(document.Name)) + if (OptionsPage.EnableTabToSpace && OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(document.Name, document.Path)) TabToSpace(wpfTextView, document.TabSize); - if (OptionsPage.EnableRemoveAndSort && OptionsPage.AllowDenyRemoveAndSortFilter.IsAllowed(document.Name) + if (OptionsPage.EnableRemoveAndSort && OptionsPage.AllowDenyRemoveAndSortFilter.IsAllowed(document.Name, document.Path) && ext == ".cs") if (!OptionsPage.EnableSmartRemoveAndSort || !HasIfCompilerDirective(wpfTextView)) RemoveAndSort(); - if (OptionsPage.EnableFormatDocument && OptionsPage.AllowDenyFormatDocumentFilter.IsAllowed(document.Name)) + if (OptionsPage.EnableFormatDocument && OptionsPage.AllowDenyFormatDocumentFilter.IsAllowed(document.Name, document.Path)) FormatDocument(ext); // Do TabToSpace again after FormatDocument, since VS2017 may stick to tab. Should remove this after VS2017 fix the bug. // At 2021.10 the bug has gone. But VS seems to stick to space now, new bug? // At 2023.01 when formatting in project/solution, with a .editorconfig set to space, // FormatDocument use tab instead. It seems FormatDocument ignore .editorconfig, and only apply VS's settings. - if (OptionsPage.EnableTabToSpace && OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(document.Name) + if (OptionsPage.EnableTabToSpace && OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(document.Name, document.Path) && document.Language == "C/C++") TabToSpace(wpfTextView, document.TabSize); - if (OptionsPage.EnableUnifyLineBreak && OptionsPage.AllowDenyUnifyLineBreakFilter.IsAllowed(document.Name)) - UnifyLineBreak(wpfTextView, OptionsPage.ForceCRLFFilter.IsAllowed(document.Name)); + if (OptionsPage.EnableUnifyLineBreak && OptionsPage.AllowDenyUnifyLineBreakFilter.IsAllowed(document.Name, document.Path)) + UnifyLineBreak(wpfTextView, OptionsPage.ForceCRLFFilter.IsAllowed(document.Name, document.Path)); - if (OptionsPage.EnableUnifyEndOfFile && OptionsPage.AllowDenyUnifyEndOfFileFilter.IsAllowed(document.Name)) + if (OptionsPage.EnableUnifyEndOfFile && OptionsPage.AllowDenyUnifyEndOfFileFilter.IsAllowed(document.Name, document.Path)) UnifyEndOfFile(wpfTextView); - if (OptionsPage.EnableForceUtf8WithBom && OptionsPage.AllowDenyForceUtf8WithBomFilter.IsAllowed(document.Name)) + if (OptionsPage.EnableForceUtf8WithBom && OptionsPage.AllowDenyForceUtf8WithBomFilter.IsAllowed(document.Name, document.Path)) ForceUtf8WithBom(wpfTextView); // Caret stay in new line but keep old column. @@ -374,7 +374,7 @@ private void FormatDocument(string ext) // In VS2022, .razor and .cshtml file will delayed Edit.FormatDocument command, which modify document after saving. // I try to capture the modification and save again. if (MajorVersion >= 17 - && !OptionsPage.ImmediateFormatDocumentFilter.IsAllowed(ext)) + && !OptionsPage.ImmediateFormatDocumentFilter.IsAllowed(ext, null)) { UpdateCaptureEvents(); diff --git a/VSFormatOnSaveShared/OptionsPage.cs b/VSFormatOnSaveShared/OptionsPage.cs index 634670c..2868b48 100644 --- a/VSFormatOnSaveShared/OptionsPage.cs +++ b/VSFormatOnSaveShared/OptionsPage.cs @@ -158,33 +158,47 @@ public enum LineBreakStyle public AllowDenyDocumentFilter AllowDenyForceUtf8WithBomFilter; + [Category("Format document")] + [DisplayName("Format Folder Blacklist")] + [Description("Files in these folders and their children are denied, unless explicitly Whitelisted. Comma separated")] + public string BlacklistFolders { get; set; } = ""; + + [Category("Format document")] + [DisplayName("Format Folder Whitelist")] + [Description("Files in these folders and their children are allowed, even if their parent folders are blacklisted. Comma separated")] + public string WhitelistFolders { get; set; } = ""; + + public AllowDenyPathFilter AllowDenyUserPaths; + public event EventHandler OnSettingsUpdated; public void UpdateSettings() { + AllowDenyUserPaths = new AllowDenyPathFilter(WhitelistFolders.Split(','), BlacklistFolders.Split(',')); + AllowDenyRemoveAndSortFilter = new AllowDenyDocumentFilter( - AllowRemoveAndSortExtensions.Split(' '), DenyRemoveAndSortExtensions.Split(' ')); + AllowRemoveAndSortExtensions.Split(' '), DenyRemoveAndSortExtensions.Split(' '), AllowDenyUserPaths); AllowDenyFormatDocumentFilter = new AllowDenyDocumentFilter( - AllowFormatDocumentExtentions.Split(' '), DenyFormatDocumentExtentions.Split(' ')); + AllowFormatDocumentExtentions.Split(' '), DenyFormatDocumentExtentions.Split(' '), AllowDenyUserPaths); - ImmediateFormatDocumentFilter = new AllowDenyDocumentFilter(null, DelayedFormatDocumentExtentions.Split(' ')); + ImmediateFormatDocumentFilter = new AllowDenyDocumentFilter(null, DelayedFormatDocumentExtentions.Split(' '), AllowDenyUserPaths); AllowDenyUnifyLineBreakFilter = new AllowDenyDocumentFilter( - AllowUnifyLineBreakExtensions.Split(' '), DenyUnifyLineBreakExtensions.Split(' ')); + AllowUnifyLineBreakExtensions.Split(' '), DenyUnifyLineBreakExtensions.Split(' '), AllowDenyUserPaths); if (string.IsNullOrWhiteSpace(ForceCRLFExtensions)) ForceCRLFExtensions = ".aspx"; - ForceCRLFFilter = new AllowDenyDocumentFilter(ForceCRLFExtensions.Split(' '), null); + ForceCRLFFilter = new AllowDenyDocumentFilter(ForceCRLFExtensions.Split(' '), null, AllowDenyUserPaths); AllowDenyUnifyEndOfFileFilter = new AllowDenyDocumentFilter( - AllowUnifyEndOfFileExtensions.Split(' '), DenyUnifyEndOfFileExtensions.Split(' ')); + AllowUnifyEndOfFileExtensions.Split(' '), DenyUnifyEndOfFileExtensions.Split(' '), AllowDenyUserPaths); AllowDenyTabToSpaceFilter = new AllowDenyDocumentFilter( - AllowTabToSpaceExtensions.Split(' '), DenyTabToSpaceExtensions.Split(' ')); + AllowTabToSpaceExtensions.Split(' '), DenyTabToSpaceExtensions.Split(' '), AllowDenyUserPaths); AllowDenyForceUtf8WithBomFilter = new AllowDenyDocumentFilter( - AllowForceUtf8WithBomExtentions.Split(' '), DenyForceUtf8WithBomExtentions.Split(' ')); + AllowForceUtf8WithBomExtentions.Split(' '), DenyForceUtf8WithBomExtentions.Split(' '), AllowDenyUserPaths); OnSettingsUpdated?.Invoke(this, null); } diff --git a/VSFormatOnSaveShared/SolutionExplorerContextMenu.cs b/VSFormatOnSaveShared/SolutionExplorerContextMenu.cs index be20032..34055df 100644 --- a/VSFormatOnSaveShared/SolutionExplorerContextMenu.cs +++ b/VSFormatOnSaveShared/SolutionExplorerContextMenu.cs @@ -145,12 +145,12 @@ private void FormatItem(object item) private void FormatProjectItem(ProjectItem item) { ThreadHelper.ThrowIfNotOnUIThread(); - if (!((_package.OptionsPage.EnableRemoveAndSort && _package.OptionsPage.AllowDenyRemoveAndSortFilter.IsAllowed(item.Name)) - || (_package.OptionsPage.EnableFormatDocument && _package.OptionsPage.AllowDenyFormatDocumentFilter.IsAllowed(item.Name)) - || (_package.OptionsPage.EnableUnifyLineBreak && _package.OptionsPage.AllowDenyUnifyLineBreakFilter.IsAllowed(item.Name)) - || (_package.OptionsPage.EnableUnifyEndOfFile && _package.OptionsPage.AllowDenyUnifyEndOfFileFilter.IsAllowed(item.Name)) - || (_package.OptionsPage.EnableTabToSpace && _package.OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(item.Name)) - || (_package.OptionsPage.EnableForceUtf8WithBom && _package.OptionsPage.AllowDenyForceUtf8WithBomFilter.IsAllowed(item.Name))) + if (!((_package.OptionsPage.EnableRemoveAndSort && _package.OptionsPage.AllowDenyRemoveAndSortFilter.IsAllowed(item.Name, item.Document.Path)) + || (_package.OptionsPage.EnableFormatDocument && _package.OptionsPage.AllowDenyFormatDocumentFilter.IsAllowed(item.Name, item.Document.Path)) + || (_package.OptionsPage.EnableUnifyLineBreak && _package.OptionsPage.AllowDenyUnifyLineBreakFilter.IsAllowed(item.Name, item.Document.Path)) + || (_package.OptionsPage.EnableUnifyEndOfFile && _package.OptionsPage.AllowDenyUnifyEndOfFileFilter.IsAllowed(item.Name, item.Document.Path)) + || (_package.OptionsPage.EnableTabToSpace && _package.OptionsPage.AllowDenyTabToSpaceFilter.IsAllowed(item.Name, item.Document.Path)) + || (_package.OptionsPage.EnableForceUtf8WithBom && _package.OptionsPage.AllowDenyForceUtf8WithBomFilter.IsAllowed(item.Name, item.Document.Path))) ) return;