Skip to content
This repository was archived by the owner on Oct 4, 2021. It is now read-only.

Commit 6a92a4f

Browse files
authored
Merge pull request #9539 from mono/roslyn-editor-config-file-support
[Ide] Support adding/updating .editorconfig with Roslyn
2 parents d15e5c6 + f9cba2d commit 6a92a4f

File tree

3 files changed

+216
-43
lines changed

3 files changed

+216
-43
lines changed

main/src/core/MonoDevelop.Ide/MonoDevelop.Ide.TypeSystem/IMonoDevelopHostDocument.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ internal static void UnRegister(ITextBuffer textBuffer)
3232
textBuffer?.Properties.RemoveProperty (typeof (IMonoDevelopHostDocument));
3333
}
3434

35-
internal static IMonoDevelopHostDocument FromDocument(Document document)
35+
internal static IMonoDevelopHostDocument FromDocument (Document document)
36+
{
37+
return FromDocument ((TextDocument)document);
38+
}
39+
40+
internal static IMonoDevelopHostDocument FromDocument(TextDocument document)
3641
{
3742
IMonoDevelopHostDocument containedDocument = null;
3843
if (document.TryGetText (out SourceText sourceText)) {

main/src/core/MonoDevelop.Ide/MonoDevelop.Ide.TypeSystem/MonoDevelopWorkspace.cs

Lines changed: 166 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ internal void ReloadModifiedProject (MonoDevelop.Projects.Project project)
486486
return await TryLoadSolution (cancellationToken).ConfigureAwait (false);
487487
}
488488

489-
async Task ReloadProjects (CancellationToken cancellationToken)
489+
internal async Task ReloadProjects (CancellationToken cancellationToken)
490490
{
491491
try {
492492
using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, src.Token);
@@ -731,9 +731,9 @@ protected override void ApplyDocumentTextChanged (DocumentId id, SourceText text
731731
tryApplyState_documentTextChangedTasks.Add (ApplyDocumentTextChangedCore (id, text));
732732
}
733733

734-
async Task ApplyDocumentTextChangedCore (DocumentId id, SourceText text)
734+
async Task ApplyDocumentTextChangedCore (DocumentId id, SourceText text, bool isAnalyzerConfigFile = false)
735735
{
736-
var document = GetDocument (id);
736+
TextDocument document = isAnalyzerConfigFile ? GetAnalyzerConfigDocument (id) : GetDocument (id);
737737
if (document == null)
738738
return;
739739

@@ -750,12 +750,21 @@ async Task ApplyDocumentTextChangedCore (DocumentId id, SourceText text)
750750
return;
751751
}
752752
}
753-
var (projection, filePath) = Projections.Get (document.FilePath);
754-
var data = TextFileProvider.Instance.GetTextEditorData (filePath, out bool isOpen);
755-
// Guard against already done changes in linked files.
756-
// This shouldn't happen but the roslyn merging seems not to be working correctly in all cases :/
757-
if (document.GetLinkedDocumentIds ().Length > 0 && isOpen && !(text.GetType ().FullName == "Microsoft.CodeAnalysis.Text.ChangedText")) {
758-
return;
753+
Projection projection = null;
754+
FilePath filePath;
755+
ITextDocument data = null;
756+
bool isOpen;
757+
if (isAnalyzerConfigFile) {
758+
filePath = document.FilePath;
759+
data = TextFileProvider.Instance.GetTextEditorData (filePath, out isOpen);
760+
} else {
761+
(projection, filePath) = Projections.Get (document.FilePath);
762+
data = TextFileProvider.Instance.GetTextEditorData (filePath, out isOpen);
763+
// Guard against already done changes in linked files.
764+
// This shouldn't happen but the roslyn merging seems not to be working correctly in all cases :/
765+
if (((Document)document).GetLinkedDocumentIds ().Length > 0 && isOpen && !(text.GetType ().FullName == "Microsoft.CodeAnalysis.Text.ChangedText")) {
766+
return;
767+
}
759768
}
760769

761770
lock (tryApplyState_documentTextChangedContents) {
@@ -799,20 +808,29 @@ async Task ApplyDocumentTextChangedCore (DocumentId id, SourceText text)
799808
}
800809
data.Save ();
801810
if (projection != null) {
802-
await UpdateProjectionsDocuments (document, data);
811+
await UpdateProjectionsDocuments ((Document)document, data);
812+
} else if (isAnalyzerConfigFile) {
813+
OnAnalyzerConfigDocumentTextChanged (id, new MonoDevelopSourceText (data), PreservationMode.PreserveValue);
803814
} else {
804815
OnDocumentTextChanged (id, new MonoDevelopSourceText (data), PreservationMode.PreserveValue);
805816
}
806817
} else {
807818
var formatter = CodeFormatterService.GetFormatter (data.MimeType);
808819
var documentContext = documentManager.Documents.FirstOrDefault (d => FilePath.PathComparer.Compare (d.FileName, filePath) == 0)?.DocumentContext;
809-
var root = await projectChanges.NewProject.GetDocument (id).GetSyntaxRootAsync ();
810-
var annotatedNode = root.DescendantNodesAndSelf ().FirstOrDefault (n => n.HasAnnotation (typeSystemService.InsertionModeAnnotation));
811-
SyntaxToken? renameTokenOpt = root.GetAnnotatedNodesAndTokens (Microsoft.CodeAnalysis.CodeActions.RenameAnnotation.Kind)
812-
.Where (s => s.IsToken)
813-
.Select (s => s.AsToken ())
814-
.Cast<SyntaxToken?> ()
815-
.FirstOrDefault ();
820+
821+
SyntaxNode root = null;
822+
SyntaxNode annotatedNode = null;
823+
SyntaxToken? renameTokenOpt = null;
824+
825+
if (!isAnalyzerConfigFile) {
826+
root = await projectChanges.NewProject.GetDocument (id).GetSyntaxRootAsync ();
827+
annotatedNode = root.DescendantNodesAndSelf ().FirstOrDefault (n => n.HasAnnotation (typeSystemService.InsertionModeAnnotation));
828+
renameTokenOpt = root.GetAnnotatedNodesAndTokens (Microsoft.CodeAnalysis.CodeActions.RenameAnnotation.Kind)
829+
.Where (s => s.IsToken)
830+
.Select (s => s.AsToken ())
831+
.Cast<SyntaxToken?> ()
832+
.FirstOrDefault ();
833+
}
816834

817835
if (documentContext != null) {
818836
var editor = (TextEditor)data;
@@ -936,12 +954,21 @@ await Runtime.RunInMainThread (async () => {
936954
}
937955

938956
if (projection != null) {
939-
await UpdateProjectionsDocuments (document, data);
957+
await UpdateProjectionsDocuments ((Document)document, data);
958+
} else if (isAnalyzerConfigFile) {
959+
OnAnalyzerConfigDocumentTextChanged (id, new MonoDevelopSourceText (data), PreservationMode.PreserveValue);
940960
} else {
941961
OnDocumentTextChanged (id, new MonoDevelopSourceText (data), PreservationMode.PreserveValue);
942962
}
943963
}
944964
}
965+
966+
protected override void ApplyAnalyzerConfigDocumentTextChanged (DocumentId id, SourceText text)
967+
{
968+
lock (projectModifyLock)
969+
tryApplyState_documentTextChangedTasks.Add (ApplyDocumentTextChangedCore (id, text, isAnalyzerConfigFile: true));
970+
}
971+
945972
internal static Func<TextEditor, int, Task<List<InsertionPoint>>> GetInsertionPoints;
946973
internal static Action<TextEditor, DocumentContext, ITextSourceVersion, SyntaxToken?> StartRenameSession;
947974

@@ -992,6 +1019,7 @@ static int ApplyChanges (Projection projection, ITextDocument data, List<Microso
9921019
HashSet<MonoDevelop.Projects.Project> tryApplyState_changedProjects = new HashSet<MonoDevelop.Projects.Project> ();
9931020
List<Task> tryApplyState_documentTextChangedTasks = new List<Task> ();
9941021
Dictionary<string, SourceText> tryApplyState_documentTextChangedContents = new Dictionary<string, SourceText> ();
1022+
HashSet<MonoDevelop.Projects.Project> tryApplyState_modifiedProjects = new HashSet<MonoDevelop.Projects.Project> ();
9951023

9961024
/// <summary>
9971025
/// Used by tests to validate that project has been saved.
@@ -1036,17 +1064,36 @@ internal override bool TryApplyChanges (Solution newSolution, IProgressTracker p
10361064
tryApplyState_documentTextChangedTasks.Clear ();
10371065
tryApplyState_changedProjects.Clear ();
10381066
freezeProjectModify = false;
1067+
if (tryApplyState_modifiedProjects.Count > 0)
1068+
NotifyProjectsModified ();
10391069
FileService.ThawEvents ();
10401070
}
10411071
}
1042-
}
1043-
1072+
}
1073+
1074+
void NotifyProjectsModified ()
1075+
{
1076+
try {
1077+
foreach (var project in tryApplyState_modifiedProjects) {
1078+
// New .editorconfig file added so we need to update the project info.
1079+
project.NotifyModified ("CoreCompileFiles");
1080+
}
1081+
} catch (Exception ex) {
1082+
LoggingService.LogError ("tryApplyState_modifiedProjects NotifyModifed error", ex);
1083+
} finally {
1084+
tryApplyState_modifiedProjects.Clear ();
1085+
}
1086+
}
1087+
10441088
public override bool CanApplyChange (ApplyChangesKind feature)
10451089
{
10461090
switch (feature) {
10471091
case ApplyChangesKind.AddDocument:
10481092
case ApplyChangesKind.RemoveDocument:
1049-
case ApplyChangesKind.ChangeDocument:
1093+
case ApplyChangesKind.ChangeDocument:
1094+
case ApplyChangesKind.AddAnalyzerConfigDocument:
1095+
case ApplyChangesKind.RemoveAnalyzerConfigDocument:
1096+
case ApplyChangesKind.ChangeAnalyzerConfigDocument:
10501097
//HACK: we don't actually support adding and removing metadata references from project
10511098
//however, our MetadataReferenceCache currently depends on (incorrectly) using TryApplyChanges
10521099
case ApplyChangesKind.AddMetadataReference:
@@ -1068,24 +1115,88 @@ protected override void ApplyProjectChanges (ProjectChanges projectChanges)
10681115

10691116
protected override void ApplyDocumentAdded (DocumentInfo info, SourceText text)
10701117
{
1071-
var id = info.Id;
1072-
MonoDevelop.Projects.Project mdProject = null;
1118+
var mdProject = GetMonoProject (info);
1119+
1120+
var path = DetermineFilePath (info, mdProject, true);
1121+
// If file is already part of project don't re-add it, example of this is .cshtml
1122+
if (mdProject?.IsFileInProject (path) == true) {
1123+
this.OnDocumentAdded (info);
1124+
return;
1125+
}
1126+
info = info.WithFilePath (path).WithTextLoader (new MonoDevelopTextLoader (path));
1127+
1128+
FormatFile (text, mdProject, path);
1129+
1130+
if (mdProject != null) {
1131+
var data = ProjectMap.GetData (info.Id.ProjectId);
1132+
data.DocumentData.Add (info.Id, path);
1133+
var file = new MonoDevelop.Projects.ProjectFile (path);
1134+
mdProject.Files.Add (file);
1135+
tryApplyState_changedProjects.Add (mdProject);
1136+
}
10731137

1138+
this.OnDocumentAdded (info);
1139+
}
1140+
1141+
MonoDevelop.Projects.Project GetMonoProject (DocumentInfo info)
1142+
{
1143+
var id = info.Id;
10741144
if (id.ProjectId != null) {
10751145
var project = CurrentSolution.GetProject (id.ProjectId);
1076-
mdProject = GetMonoProject (project);
1146+
var mdProject = GetMonoProject (project);
10771147
if (mdProject == null)
1078-
LoggingService.LogWarning ("Couldn't find project for newly generated file {0} (Project {1}).", info.Name, info.Id.ProjectId);
1148+
LoggingService.LogWarning ("Couldn't find project for document {0} (Project {1}).", info.Name, id.ProjectId);
1149+
return mdProject;
10791150
}
1151+
return null;
1152+
}
10801153

1081-
var path = DetermineFilePath (info.Id, info.Name, info.FilePath, info.Folders, mdProject?.FileName.ParentDirectory, true);
1082-
// If file is already part of project don't re-add it, example of this is .cshtml
1083-
if (mdProject?.IsFileInProject (path) == true) {
1084-
this.OnDocumentAdded (info);
1154+
protected override void ApplyAnalyzerConfigDocumentAdded (DocumentInfo info, SourceText text)
1155+
{
1156+
var mdProject = GetMonoProject (info);
1157+
1158+
var path = DetermineFilePath (info, mdProject, true);
1159+
// If file is already part of project don't re-add it unless it does not exist.
1160+
if (mdProject?.IsFileInProject (path) == true && File.Exists (path)) {
1161+
OnAnalyzerConfigDocumentAdded (info);
10851162
return;
10861163
}
1087-
info = info.WithFilePath (path).WithTextLoader (new MonoDevelopTextLoader (path));
1164+
info = info.WithFilePath (path).WithTextLoader (new MonoDevelopTextLoader (path));
1165+
1166+
FormatFile (text, mdProject, path);
10881167

1168+
if (mdProject != null) {
1169+
var data = ProjectMap.GetData (info.Id.ProjectId);
1170+
data.DocumentData.Add (info.Id, path);
1171+
1172+
var file = new MonoDevelop.Projects.ProjectFile (path, MonoDevelop.Projects.BuildAction.None);
1173+
if (!file.FilePath.IsChildPathOf (mdProject.BaseDirectory)) {
1174+
// Outside project directory - add it as a solution folder item.
1175+
var solutionFolder = GetSolutionItemsFolder (mdProject);
1176+
if (!solutionFolder.Files.Contains (path)) {
1177+
solutionFolder.Files.Add (path);
1178+
mdProject.ParentSolution.SaveAsync (new ProgressMonitor ()).Ignore ();
1179+
}
1180+
}
1181+
if (!mdProject.IsFileInProject (file.FilePath)) {
1182+
mdProject.Files.Add (file);
1183+
}
1184+
// Need to trigger Project.Modified event after TryApp is run so project is reloaded by the type system
1185+
// service. Adding the file to the Files collection will not trigger the modified event since
1186+
// the build action is not EditorConfigFiles. Also this event would be ignored anyway during TryApply.
1187+
// Also handles if the link already existed. Need to refresh all projects since the document added
1188+
// method will only be called once.
1189+
foreach (var affectedProject in mdProject.ParentSolution.GetAllProjects ()) {
1190+
tryApplyState_modifiedProjects.Add (affectedProject);
1191+
}
1192+
tryApplyState_changedProjects.Add (mdProject);
1193+
}
1194+
1195+
OnAnalyzerConfigDocumentAdded (info);
1196+
}
1197+
1198+
void FormatFile (SourceText text, MonoDevelop.Projects.Project mdProject, string path)
1199+
{
10891200
string formattedText;
10901201
var formatter = CodeFormatterService.GetFormatter (desktopService.GetMimeTypeForUri (path));
10911202
if (formatter != null && mdProject != null) {
@@ -1100,16 +1211,22 @@ protected override void ApplyDocumentAdded (DocumentInfo info, SourceText text)
11001211
} catch (Exception e) {
11011212
LoggingService.LogError ("Exception while saving file to " + path, e);
11021213
}
1214+
}
11031215

1104-
if (mdProject != null) {
1105-
var data = ProjectMap.GetData (id.ProjectId);
1106-
data.DocumentData.Add (info.Id, path);
1107-
var file = new MonoDevelop.Projects.ProjectFile (path);
1108-
mdProject.Files.Add (file);
1109-
tryApplyState_changedProjects.Add (mdProject);
1216+
MonoDevelop.Projects.SolutionFolder GetSolutionItemsFolder (MonoDevelop.Projects.Project project)
1217+
{
1218+
string name = GettextCatalog.GetString ("Solution Items");
1219+
var folder = project.ParentSolution.RootFolder.Items
1220+
.OfType <MonoDevelop.Projects.SolutionFolder> ()
1221+
.FirstOrDefault (item => StringComparer.CurrentCultureIgnoreCase.Equals (item.Name, name));
1222+
if (folder != null) {
1223+
return folder;
11101224
}
11111225

1112-
this.OnDocumentAdded (info);
1226+
folder = new MonoDevelop.Projects.SolutionFolder ();
1227+
folder.Name = name;
1228+
project.ParentSolution.RootFolder.Items.Add (folder);
1229+
return folder;
11131230
}
11141231

11151232
protected override void ApplyDocumentRemoved (DocumentId documentId)
@@ -1139,16 +1256,23 @@ protected override void ApplyDocumentRemoved (DocumentId documentId)
11391256
tryApplyState_changedProjects.Add (mdProject);
11401257
}
11411258

1142-
string DetermineFilePath (DocumentId id, string name, string filePath, IReadOnlyList<string> docFolders, string defaultFolder, bool createDirectory = false)
1259+
protected override void ApplyAnalyzerConfigDocumentRemoved (DocumentId documentId)
1260+
{
1261+
LoggingService.LogInfo ("ApplyAnalyzerConfigDocumentRemoved {0}", documentId);
1262+
base.ApplyAnalyzerConfigDocumentRemoved (documentId);
1263+
}
1264+
1265+
string DetermineFilePath (DocumentInfo info, MonoDevelop.Projects.Project mdProject, bool createDirectory = false)
11431266
{
1144-
var path = filePath;
1267+
var path = info.FilePath;
11451268

11461269
if (string.IsNullOrEmpty (path)) {
1147-
var monoProject = GetMonoProject (id.ProjectId);
1270+
var monoProject = GetMonoProject (info.Id.ProjectId);
11481271

11491272
// If the first namespace name matches the name of the project, then we don't want to
11501273
// generate a folder for that. The project is implicitly a folder with that name.
11511274
IEnumerable<string> folders;
1275+
var docFolders = info.Folders;
11521276
if (docFolders != null && monoProject != null && docFolders.FirstOrDefault () == monoProject.Name) {
11531277
folders = docFolders.Skip (1);
11541278
} else {
@@ -1163,9 +1287,9 @@ string DetermineFilePath (DocumentId id, string name, string filePath, IReadOnly
11631287
} catch (Exception e) {
11641288
LoggingService.LogError ("Error while creating directory for a new file : " + baseDirectory, e);
11651289
}
1166-
path = Path.Combine (baseDirectory, name);
1290+
path = Path.Combine (baseDirectory, info.Name);
11671291
} else {
1168-
path = Path.Combine (defaultFolder, name);
1292+
path = Path.Combine (mdProject?.FileName.ParentDirectory, info.Name);
11691293
}
11701294
}
11711295
return path;

0 commit comments

Comments
 (0)