|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.GenerateType;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.ProjectManagement;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.GenerateType;
internal class GenerateTypeDialogViewModel : AbstractNotifyPropertyChanged
{
private readonly Document _document;
private readonly INotificationService _notificationService;
private readonly IProjectManagementService _projectManagementService;
private readonly ISyntaxFactsService _syntaxFactsService;
private readonly GenerateTypeDialogOptions _generateTypeDialogOptions;
private string _typeName;
private bool _isNewFile;
private Dictionary<string, Accessibility> _accessListMap;
private Dictionary<string, TypeKind> _typeKindMap;
private List<string> _csharpAccessList;
private List<string> _visualBasicAccessList;
private List<string> _csharpTypeKindList;
private List<string> _visualBasicTypeKindList;
private readonly string _csharpExtension = ".cs";
private readonly string _visualBasicExtension = ".vb";
// reserved names that cannot be a folder name or filename
private readonly string[] _reservedKeywords =
#pragma warning disable format // https://github.com/dotnet/roslyn/issues/70711 tracks removing this suppression.
[
"con", "prn", "aux", "nul",
"com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",
"lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "clock$"
];
#pragma warning restore format
// Below code details with the Access List and the manipulation
public List<string> AccessList { get; }
private int _accessSelectIndex;
public int AccessSelectIndex
{
get
{
return _accessSelectIndex;
}
set
{
SetProperty(ref _accessSelectIndex, value);
}
}
private string _selectedAccessibilityString;
public string SelectedAccessibilityString
{
get
{
return _selectedAccessibilityString;
}
set
{
SetProperty(ref _selectedAccessibilityString, value);
}
}
public Accessibility SelectedAccessibility
{
get
{
Debug.Assert(_accessListMap.ContainsKey(SelectedAccessibilityString), "The Accessibility Key String not present");
return _accessListMap[SelectedAccessibilityString];
}
}
private List<string> _kindList;
public List<string> KindList
{
get
{
return _kindList;
}
set
{
SetProperty(ref _kindList, value);
}
}
private int _kindSelectIndex;
public int KindSelectIndex
{
get
{
return _kindSelectIndex;
}
set
{
SetProperty(ref _kindSelectIndex, value);
}
}
private string _selectedTypeKindString;
public string SelectedTypeKindString
{
get
{
return _selectedTypeKindString;
}
set
{
SetProperty(ref _selectedTypeKindString, value);
}
}
public TypeKind SelectedTypeKind
{
get
{
Debug.Assert(_typeKindMap.ContainsKey(SelectedTypeKindString), "The TypeKind Key String not present");
return _typeKindMap[SelectedTypeKindString];
}
}
private void PopulateTypeKind(TypeKind typeKind, string csharpKey, string visualBasicKey)
{
_typeKindMap.Add(visualBasicKey, typeKind);
_typeKindMap.Add(csharpKey, typeKind);
_csharpTypeKindList.Add(csharpKey);
_visualBasicTypeKindList.Add(visualBasicKey);
}
private void PopulateTypeKind(TypeKind typeKind, string visualBasicKey)
{
_typeKindMap.Add(visualBasicKey, typeKind);
_visualBasicTypeKindList.Add(visualBasicKey);
}
private void PopulateAccessList(string key, Accessibility accessibility, string languageName = null)
{
if (languageName == null)
{
_csharpAccessList.Add(key);
_visualBasicAccessList.Add(key);
}
else if (languageName == LanguageNames.CSharp)
{
_csharpAccessList.Add(key);
}
else
{
Debug.Assert(languageName == LanguageNames.VisualBasic, "Currently only C# and VB are supported");
_visualBasicAccessList.Add(key);
}
_accessListMap.Add(key, accessibility);
}
private void InitialSetup()
{
_accessListMap = [];
_typeKindMap = [];
_csharpAccessList = [];
_visualBasicAccessList = [];
_csharpTypeKindList = [];
_visualBasicTypeKindList = [];
// Populate the AccessListMap
if (!_generateTypeDialogOptions.IsPublicOnlyAccessibility)
{
PopulateAccessList("Default", Accessibility.NotApplicable);
PopulateAccessList("internal", Accessibility.Internal, LanguageNames.CSharp);
PopulateAccessList("Friend", Accessibility.Internal, LanguageNames.VisualBasic);
}
PopulateAccessList("public", Accessibility.Public, LanguageNames.CSharp);
PopulateAccessList("Public", Accessibility.Public, LanguageNames.VisualBasic);
// Populate the TypeKind
PopulateTypeKind();
}
private void PopulateTypeKind()
{
Debug.Assert(_generateTypeDialogOptions.TypeKindOptions != TypeKindOptions.None);
if (TypeKindOptionsHelper.IsClass(_generateTypeDialogOptions.TypeKindOptions))
{
PopulateTypeKind(TypeKind.Class, "class", "Class");
}
if (TypeKindOptionsHelper.IsEnum(_generateTypeDialogOptions.TypeKindOptions))
{
PopulateTypeKind(TypeKind.Enum, "enum", "Enum");
}
if (TypeKindOptionsHelper.IsStructure(_generateTypeDialogOptions.TypeKindOptions))
{
PopulateTypeKind(TypeKind.Structure, "struct", "Structure");
}
if (TypeKindOptionsHelper.IsInterface(_generateTypeDialogOptions.TypeKindOptions))
{
PopulateTypeKind(TypeKind.Interface, "interface", "Interface");
}
if (TypeKindOptionsHelper.IsDelegate(_generateTypeDialogOptions.TypeKindOptions))
{
PopulateTypeKind(TypeKind.Delegate, "delegate", "Delegate");
}
if (TypeKindOptionsHelper.IsModule(_generateTypeDialogOptions.TypeKindOptions))
{
_shouldChangeTypeKindListSelectedIndex = true;
PopulateTypeKind(TypeKind.Module, "Module");
}
}
internal bool TrySubmit()
{
if (this.IsNewFile)
{
var trimmedFileName = FileName.Trim();
// Case : \\Something
if (trimmedFileName.StartsWith(@"\\", StringComparison.Ordinal))
{
SendFailureNotification(ServicesVSResources.Illegal_characters_in_path);
return false;
}
// Case : something\
if (string.IsNullOrWhiteSpace(trimmedFileName) || trimmedFileName.EndsWith(@"\", StringComparison.Ordinal))
{
SendFailureNotification(ServicesVSResources.Path_cannot_have_empty_filename);
return false;
}
if (trimmedFileName.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
SendFailureNotification(ServicesVSResources.Illegal_characters_in_path);
return false;
}
var isRootOfTheProject = trimmedFileName.StartsWith(@"\", StringComparison.Ordinal);
string implicitFilePath = null;
// Construct the implicit file path
if (isRootOfTheProject || this.SelectedProject != _document.Project)
{
if (!TryGetImplicitFilePath(this.SelectedProject.FilePath ?? string.Empty, ServicesVSResources.Project_Path_is_illegal, out implicitFilePath))
{
return false;
}
}
else
{
if (!TryGetImplicitFilePath(_document.FilePath, ServicesVSResources.DocumentPath_is_illegal, out implicitFilePath))
{
return false;
}
}
// Remove the '\' at the beginning if present
trimmedFileName = trimmedFileName.StartsWith(@"\", StringComparison.Ordinal) ? trimmedFileName[1..] : trimmedFileName;
// Construct the full path of the file to be created
this.FullFilePath = implicitFilePath + @"\" + trimmedFileName;
try
{
this.FullFilePath = Path.GetFullPath(this.FullFilePath);
}
catch (Exception e)
{
SendFailureNotification(e.Message);
return false;
}
// Path.GetFullPath does not remove the spaces infront of the filename or folder name . So remove it
var lastIndexOfSeparatorInFullPath = this.FullFilePath.LastIndexOf('\\');
if (lastIndexOfSeparatorInFullPath != -1)
{
var fileNameInFullPathInContainers = this.FullFilePath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
// Trim spaces of each component of the file name.
// Note that path normalization changed between 4.6.1 and 4.6.2 and GetFullPath no longer trims trailing spaces.
// See https://blogs.msdn.microsoft.com/jeremykuhne/2016/06/21/more-on-new-net-path-handling/
this.FullFilePath = string.Join("\\", fileNameInFullPathInContainers.Select(str => str.Trim()));
}
string projectRootPath = null;
if (this.SelectedProject.FilePath == null)
{
projectRootPath = string.Empty;
}
else if (!TryGetImplicitFilePath(this.SelectedProject.FilePath, ServicesVSResources.Project_Path_is_illegal, out projectRootPath))
{
return false;
}
if (this.FullFilePath.StartsWith(projectRootPath, StringComparison.Ordinal))
{
// The new file will be within the root of the project
var folderPath = this.FullFilePath[projectRootPath.Length..];
var containers = folderPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
// Folder name was mentioned
if (containers.Length > 1)
{
_fileName = containers.Last();
Folders = new List<string>(containers);
Folders.RemoveAt(Folders.Count - 1);
if (Folders.Any(folder => !(_syntaxFactsService.IsValidIdentifier(folder) || _syntaxFactsService.IsVerbatimIdentifier(folder))))
{
_areFoldersValidIdentifiers = false;
}
}
else if (containers.Length == 1)
{
// File goes at the root of the Directory
_fileName = containers[0];
Folders = null;
}
else
{
SendFailureNotification(ServicesVSResources.Illegal_characters_in_path);
return false;
}
}
else
{
// The new file will be outside the root of the project and folders will be null
Folders = null;
var lastIndexOfSeparator = this.FullFilePath.LastIndexOf('\\');
if (lastIndexOfSeparator == -1)
{
SendFailureNotification(ServicesVSResources.Illegal_characters_in_path);
return false;
}
_fileName = this.FullFilePath[(lastIndexOfSeparator + 1)..];
}
// Check for reserved words in the folder or filename
if (this.FullFilePath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries).Any(s => _reservedKeywords.Contains(s, StringComparer.OrdinalIgnoreCase)))
{
SendFailureNotification(ServicesVSResources.File_path_cannot_use_reserved_keywords);
return false;
}
// We check to see if file path of the new file matches the filepath of any other existing file or if the Folders and FileName matches any of the document then
// we say that the file already exists.
if (this.SelectedProject.Documents.Where(n => n != null).Where(n => n.FilePath == FullFilePath).Any() ||
(this.Folders != null && this.FileName != null &&
this.SelectedProject.Documents.Where(n => n.Name != null && n.Folders.Count > 0 && n.Name == this.FileName && this.Folders.SequenceEqual(n.Folders)).Any()) ||
File.Exists(FullFilePath))
{
SendFailureNotification(ServicesVSResources.File_already_exists);
return false;
}
}
return true;
}
private bool TryGetImplicitFilePath(string implicitPathContainer, string message, out string implicitPath)
{
var indexOfLastSeparator = implicitPathContainer.LastIndexOf('\\');
if (indexOfLastSeparator == -1)
{
SendFailureNotification(message);
implicitPath = null;
return false;
}
implicitPath = implicitPathContainer[..indexOfLastSeparator];
return true;
}
private void SendFailureNotification(string message)
=> _notificationService.SendNotification(message, severity: NotificationSeverity.Information);
private Project _selectedProject;
public Project SelectedProject
{
get
{
return _selectedProject;
}
set
{
var previousProject = _selectedProject;
if (SetProperty(ref _selectedProject, value))
{
NotifyPropertyChanged(nameof(DocumentList));
this.DocumentSelectIndex = 0;
this.ProjectSelectIndex = this.ProjectList.FindIndex(p => p.Project == _selectedProject);
if (_selectedProject != _document.Project)
{
// Restrict the Access List Options
// 3 in the list represent the Public. 1-based array.
this.AccessSelectIndex = this.AccessList.IndexOf("public") == -1 ?
this.AccessList.IndexOf("Public") : this.AccessList.IndexOf("public");
Debug.Assert(this.AccessSelectIndex != -1);
this.IsAccessListEnabled = false;
}
else
{
// Remove restriction
this.IsAccessListEnabled = true;
}
if (previousProject != null && _projectManagementService != null)
{
this.ProjectFolders = _projectManagementService.GetFolders(this.SelectedProject.Id, this.SelectedProject.Solution.Workspace);
}
// Update the TypeKindList if required
if (previousProject != null && previousProject.Language != _selectedProject.Language)
{
if (_selectedProject.Language == LanguageNames.CSharp)
{
var previousSelectedIndex = _kindSelectIndex;
this.KindList = _csharpTypeKindList;
if (_shouldChangeTypeKindListSelectedIndex)
{
this.KindSelectIndex = 0;
}
else
{
this.KindSelectIndex = previousSelectedIndex;
}
}
else
{
var previousSelectedIndex = _kindSelectIndex;
this.KindList = _visualBasicTypeKindList;
if (_shouldChangeTypeKindListSelectedIndex)
{
this.KindSelectIndex = 0;
}
else
{
this.KindSelectIndex = previousSelectedIndex;
}
}
}
// Update File Extension
UpdateFileNameExtension();
}
}
}
private int _projectSelectIndex;
public int ProjectSelectIndex
{
get
{
return _projectSelectIndex;
}
set
{
SetProperty(ref _projectSelectIndex, value);
}
}
public List<ProjectSelectItem> ProjectList { get; }
private Project _previouslyPopulatedProject = null;
private List<DocumentSelectItem> _previouslyPopulatedDocumentList = null;
public IEnumerable<DocumentSelectItem> DocumentList
{
get
{
if (_previouslyPopulatedProject == _selectedProject)
{
return _previouslyPopulatedDocumentList;
}
_previouslyPopulatedProject = _selectedProject;
_previouslyPopulatedDocumentList = [];
// Check for the current project
if (_selectedProject == _document.Project)
{
// populate the current document
_previouslyPopulatedDocumentList.Add(new DocumentSelectItem(_document, "<Current File>"));
// Set the initial selected Document
this.SelectedDocument = _document;
// Populate the rest of the documents for the project
_previouslyPopulatedDocumentList.AddRange(_document.Project.Documents
.Where(d => d != _document && !d.IsGeneratedCode(CancellationToken.None))
.Select(d => new DocumentSelectItem(d)));
}
else
{
_previouslyPopulatedDocumentList.AddRange(_selectedProject.Documents
.Where(d => !d.IsGeneratedCode(CancellationToken.None))
.Select(d => new DocumentSelectItem(d)));
this.SelectedDocument = _selectedProject.Documents.FirstOrDefault();
}
this.IsExistingFileEnabled = _previouslyPopulatedDocumentList.Count == 0 ? false : true;
this.IsNewFile = this.IsExistingFileEnabled ? this.IsNewFile : true;
return _previouslyPopulatedDocumentList;
}
}
private bool _isExistingFileEnabled = true;
public bool IsExistingFileEnabled
{
get
{
return _isExistingFileEnabled;
}
set
{
SetProperty(ref _isExistingFileEnabled, value);
}
}
private int _documentSelectIndex;
public int DocumentSelectIndex
{
get
{
return _documentSelectIndex;
}
set
{
SetProperty(ref _documentSelectIndex, value);
}
}
private Document _selectedDocument;
public Document SelectedDocument
{
get
{
return _selectedDocument;
}
set
{
SetProperty(ref _selectedDocument, value);
}
}
private string _fileName;
public string FileName
{
get
{
return _fileName;
}
set
{
SetProperty(ref _fileName, value);
}
}
public List<string> Folders;
public string TypeName
{
get
{
return _typeName;
}
set
{
SetProperty(ref _typeName, value);
}
}
public bool IsNewFile
{
get
{
return _isNewFile;
}
set
{
SetProperty(ref _isNewFile, value);
}
}
public bool IsExistingFile
{
get
{
return !_isNewFile;
}
set
{
SetProperty(ref _isNewFile, !value);
}
}
private bool _isAccessListEnabled;
private bool _shouldChangeTypeKindListSelectedIndex = false;
public bool IsAccessListEnabled
{
get
{
return _isAccessListEnabled;
}
set
{
SetProperty(ref _isAccessListEnabled, value);
}
}
private bool _areFoldersValidIdentifiers = true;
public bool AreFoldersValidIdentifiers
{
get
{
if (_areFoldersValidIdentifiers)
{
/*
var workspace = this.SelectedProject.Solution.Workspace as VisualStudioWorkspaceImpl;
var project = workspace?.GetHostProject(this.SelectedProject.Id) as AbstractProject;
return !(project?.IsWebSite == true);
*/
return false;
}
return false;
}
}
public IList<string> ProjectFolders { get; private set; }
public string FullFilePath { get; private set; }
internal void UpdateFileNameExtension()
{
var currentFileName = this.FileName.Trim();
if (!string.IsNullOrWhiteSpace(currentFileName) && !currentFileName.EndsWith("\\", StringComparison.Ordinal))
{
if (this.SelectedProject.Language == LanguageNames.CSharp)
{
// For CSharp
currentFileName = UpdateExtension(currentFileName, _csharpExtension, _visualBasicExtension);
}
else
{
// For Visual Basic
currentFileName = UpdateExtension(currentFileName, _visualBasicExtension, _csharpExtension);
}
}
this.FileName = currentFileName;
}
private static string UpdateExtension(string currentFileName, string desiredFileExtension, string undesiredFileExtension)
{
if (currentFileName.EndsWith(desiredFileExtension, StringComparison.OrdinalIgnoreCase))
{
// No change required
return currentFileName;
}
// Remove the undesired extension
if (currentFileName.EndsWith(undesiredFileExtension, StringComparison.OrdinalIgnoreCase))
{
currentFileName = currentFileName[..^undesiredFileExtension.Length];
}
// Append the desired extension
return currentFileName + desiredFileExtension;
}
internal GenerateTypeDialogViewModel(
Document document,
INotificationService notificationService,
IProjectManagementService projectManagementService,
ISyntaxFactsService syntaxFactsService,
GenerateTypeDialogOptions generateTypeDialogOptions,
string typeName,
string fileExtension,
bool isNewFile,
string accessSelectString,
string typeKindSelectString)
{
_generateTypeDialogOptions = generateTypeDialogOptions;
InitialSetup();
var dependencyGraph = document.Project.Solution.GetProjectDependencyGraph();
// Initialize the dependencies
var projectListing = new List<ProjectSelectItem>
{
// Populate the project list
// Add the current project
new ProjectSelectItem(document.Project)
};
// Add the rest of the projects
// Adding dependency graph to avoid cyclic dependency
projectListing.AddRange(document.Project.Solution.Projects
.Where(p => p != document.Project && !dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(p.Id).Contains(document.Project.Id))
.Select(p => new ProjectSelectItem(p)));
this.ProjectList = projectListing;
const string attributeSuffix = "Attribute";
_typeName = generateTypeDialogOptions.IsAttribute && !typeName.EndsWith(attributeSuffix, StringComparison.Ordinal) ? typeName + attributeSuffix : typeName;
this.FileName = typeName + fileExtension;
_document = document;
this.SelectedProject = document.Project;
this.SelectedDocument = document;
_notificationService = notificationService;
this.AccessList = document.Project.Language == LanguageNames.CSharp
? _csharpAccessList
: _visualBasicAccessList;
this.AccessSelectIndex = this.AccessList.Contains(accessSelectString)
? this.AccessList.IndexOf(accessSelectString) : 0;
this.IsAccessListEnabled = true;
this.KindList = document.Project.Language == LanguageNames.CSharp
? _csharpTypeKindList
: _visualBasicTypeKindList;
this.KindSelectIndex = this.KindList.Contains(typeKindSelectString)
? this.KindList.IndexOf(typeKindSelectString) : 0;
this.ProjectSelectIndex = 0;
this.DocumentSelectIndex = 0;
_isNewFile = isNewFile;
_syntaxFactsService = syntaxFactsService;
_projectManagementService = projectManagementService;
if (projectManagementService != null)
{
this.ProjectFolders = _projectManagementService.GetFolders(this.SelectedProject.Id, this.SelectedProject.Solution.Workspace);
}
else
{
this.ProjectFolders = [];
}
}
public class ProjectSelectItem
{
public string Name
{
get
{
return Project.Name;
}
}
public Project Project { get; }
public ProjectSelectItem(Project project)
=> Project = project;
}
public class DocumentSelectItem
{
public Document Document { get; }
public string Name { get; }
public DocumentSelectItem(Document document, string documentName)
{
Document = document;
Name = documentName;
}
public DocumentSelectItem(Document document)
{
Document = document;
if (document.Folders.Count == 0)
{
Name = document.Name;
}
else
{
Name = string.Join("\\", document.Folders) + "\\" + document.Name;
}
}
}
}
|