diff --git a/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestClass.cs b/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestClass.cs index f9f604633d..e78de022b7 100644 --- a/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestClass.cs +++ b/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestClass.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; + using ICSharpCode.NRefactory.TypeSystem; using ICSharpCode.NRefactory.TypeSystem.Implementation; using ICSharpCode.SharpDevelop; +using ICSharpCode.SharpDevelop.Dom; +using ICSharpCode.SharpDevelop.Parser; using ICSharpCode.SharpDevelop.Widgets; namespace ICSharpCode.UnitTesting @@ -67,7 +70,12 @@ namespace ICSharpCode.UnitTesting public ITypeDefinition Resolve() { - ICompilation compilation = SD.ParserService.GetCompilation(parentProject.Project); + return Resolve(SD.ParserService.GetCurrentSolutionSnapshot()); + } + + public ITypeDefinition Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + ICompilation compilation = solutionSnapshot.GetCompilation(parentProject.Project); IType type = compilation.MainAssembly.GetTypeDefinition(fullTypeName); return type.GetDefinition(); } diff --git a/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestMethod.cs b/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestMethod.cs index 82daccee8a..01a69492ea 100644 --- a/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestMethod.cs +++ b/src/AddIns/Analysis/UnitTesting/NUnit/NUnitTestMethod.cs @@ -5,6 +5,7 @@ using System; using System.Windows.Input; using ICSharpCode.NRefactory.TypeSystem; using ICSharpCode.SharpDevelop; +using ICSharpCode.SharpDevelop.Parser; using ICSharpCode.SharpDevelop.Widgets; namespace ICSharpCode.UnitTesting @@ -80,7 +81,12 @@ namespace ICSharpCode.UnitTesting public IMethod Resolve() { - ICompilation compilation = SD.ParserService.GetCompilation(parentProject.Project); + return Resolve(SD.ParserService.GetCurrentSolutionSnapshot()); + } + + public IMethod Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + ICompilation compilation = solutionSnapshot.GetCompilation(parentProject.Project); IMethod resolvedMethod = method.Resolve(new SimpleTypeResolveContext(compilation.MainAssembly)); return resolvedMethod; } diff --git a/src/Main/Base/Project/Dom/ConcatModelCollection.cs b/src/Main/Base/Project/Dom/ConcatModelCollection.cs new file mode 100644 index 0000000000..9e4c4218d1 --- /dev/null +++ b/src/Main/Base/Project/Dom/ConcatModelCollection.cs @@ -0,0 +1,164 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// A that works by concatening multiple + /// other model collections. + /// + public sealed class ConcatModelCollection : IModelCollection + { + sealed class InputCollection : Collection> + { + readonly ConcatModelCollection owner; + + public InputCollection(ConcatModelCollection owner) + { + this.owner = owner; + } + + protected override void ClearItems() + { + if (owner.collectionChanged != null) { + foreach (var input in Items) { + input.CollectionChanged -= owner.OnInputCollectionChanged; + } + } + base.ClearItems(); + owner.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override void InsertItem(int index, IModelCollection item) + { + if (owner.collectionChanged != null) + item.CollectionChanged += owner.OnInputCollectionChanged; + base.InsertItem(index, item); + owner.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, item.ToArray(), owner.GetCount(index))); + } + + protected override void RemoveItem(int index) + { + if (owner.collectionChanged != null) + Items[index].CollectionChanged -= owner.OnInputCollectionChanged; + var oldItems = Items[index].ToArray(); + base.RemoveItem(index); + owner.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, oldItems, owner.GetCount(index))); + } + + protected override void SetItem(int index, IModelCollection item) + { + RemoveItem(index); + InsertItem(index, item); + } + } + + InputCollection inputs; + + public ConcatModelCollection() + { + this.inputs = new InputCollection(this); + } + + public IList> Inputs { + get { return inputs; } + } + + NotifyCollectionChangedEventHandler collectionChanged; + + public event NotifyCollectionChangedEventHandler CollectionChanged { + add { + var oldEventHandlers = collectionChanged; + collectionChanged += value; + if (oldEventHandlers == null && collectionChanged != null) { + foreach (var input in inputs) + input.CollectionChanged += OnInputCollectionChanged; + } + } + remove { + var oldEventHandlers = collectionChanged; + collectionChanged -= value; + if (oldEventHandlers != null && collectionChanged == null) { + foreach (var input in inputs) + input.CollectionChanged -= OnInputCollectionChanged; + } + } + } + + void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (collectionChanged != null) + collectionChanged(this, e); + } + + void OnInputCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + int inputIndex = inputs.IndexOf((IModelCollection)sender); + int startIndex = GetCount(inputIndex); + NotifyCollectionChangedEventArgs newEventArgs; + switch (e.Action) { + case NotifyCollectionChangedAction.Add: + newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, e.NewItems, startIndex + e.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Remove: + newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, e.OldItems, startIndex + e.OldStartingIndex); + break; + case NotifyCollectionChangedAction.Replace: + newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, e.OldItems, e.NewItems, startIndex + e.OldStartingIndex); + break; + case NotifyCollectionChangedAction.Move: + newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, e.OldItems, startIndex + e.OldStartingIndex, startIndex + e.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Reset: + newEventArgs = new NotifyCollectionChangedEventArgs(e.Action); + break; + default: + throw new NotSupportedException("Invalid value for NotifyCollectionChangedAction"); + } + collectionChanged(this, newEventArgs); + } + + public int Count { + get { return GetCount(inputs.Count); } + } + + int GetCount(int inputIndex) + { + int count = 0; + for (int i = 0; i < inputIndex; i++) { + count += inputs[i].Count; + } + return count; + } + + public IEnumerator GetEnumerator() + { + return inputs.SelectMany(i => i).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public T this[int index] { + get { + int inputIndex = 0; + while (index >= inputs[inputIndex].Count) { + index -= inputs[inputIndex].Count; + inputIndex++; + } + return inputs[inputIndex][index]; + } + } + } +} diff --git a/src/Main/Base/Project/Dom/IEntityModel.cs b/src/Main/Base/Project/Dom/IEntityModel.cs new file mode 100644 index 0000000000..7f1f05be19 --- /dev/null +++ b/src/Main/Base/Project/Dom/IEntityModel.cs @@ -0,0 +1,40 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; +using ICSharpCode.SharpDevelop.Project; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// An NRefactory entity as a model. + /// + public interface IEntityModel : INotifyPropertyChanged + { + /// + /// Gets the parent project that contains this entity. + /// May return null if the entity is not part of a project. + /// + IProject ParentProject { get; } + + /// + /// Gets the region where this entity is defined. + /// + DomRegion Region { get; } + + /// + /// Resolves the entity in the current solution snapshot. + /// Returns null if the entity could not be resolved. + /// + IEntity Resolve(); + + /// + /// Resolves the entity in the specified solution snapshot. + /// Returns null if the entity could not be resolved. + /// + IEntity Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot); + } +} diff --git a/src/Main/Base/Project/Dom/IEntityModelContext.cs b/src/Main/Base/Project/Dom/IEntityModelContext.cs new file mode 100644 index 0000000000..cb3f299693 --- /dev/null +++ b/src/Main/Base/Project/Dom/IEntityModelContext.cs @@ -0,0 +1,79 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; +using ICSharpCode.SharpDevelop.Project; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// The context for an entity model. + /// This may be a reference to a project, or a compilation provider for a single stand-alone code file. + /// + public interface IEntityModelContext + { + /// + /// Used for . + /// + IProject Project { get; } + + /// + /// Used for . + /// + /// + /// The solution snapshot provided to , + /// or null if the overload was used. + /// + ICompilation GetCompilation(ISolutionSnapshotWithProjectMapping solutionSnapshot); + + /// + /// Returns true if part1 is considered a better candidate for the primary part than part2. + /// + bool IsBetterPart(IUnresolvedTypeDefinition part1, IUnresolvedTypeDefinition part2); + } + + public class ProjectEntityModelContext : IEntityModelContext + { + readonly IProject project; + readonly string primaryCodeFileExtension; + + public ProjectEntityModelContext(IProject project, string primaryCodeFileExtension) + { + if (project == null) + throw new ArgumentNullException("project"); + this.project = project; + this.primaryCodeFileExtension = primaryCodeFileExtension; + } + + public IProject Project { + get { return project; } + } + + public ICompilation GetCompilation(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + if (solutionSnapshot != null) + return solutionSnapshot.GetCompilation(project); + else + return SD.ParserService.GetCompilation(project); + } + + public bool IsBetterPart(IUnresolvedTypeDefinition part1, IUnresolvedTypeDefinition part2) + { + IUnresolvedFile file1 = part1.UnresolvedFile; + IUnresolvedFile file2 = part2.UnresolvedFile; + if (file1 != null && file2 == null) + return true; + if (file1 == null) + return false; + bool file1HasExtension = file1.FileName.EndsWith(primaryCodeFileExtension, StringComparison.OrdinalIgnoreCase); + bool file2HasExtension = file2.FileName.EndsWith(primaryCodeFileExtension, StringComparison.OrdinalIgnoreCase); + if (file1HasExtension && !file2HasExtension) + return true; + if (!file1HasExtension && file2HasExtension) + return false; + return file1.FileName.Length < file2.FileName.Length; + } + } +} diff --git a/src/Main/Base/Project/Dom/IMemberModel.cs b/src/Main/Base/Project/Dom/IMemberModel.cs new file mode 100644 index 0000000000..9a719fa599 --- /dev/null +++ b/src/Main/Base/Project/Dom/IMemberModel.cs @@ -0,0 +1,27 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// Observable model for a member. + /// + public interface IMemberModel : IEntityModel + { + /// + /// Resolves the member in the current solution snapshot. + /// Returns null if the member could not be resolved. + /// + IMember Resolve(); + + /// + /// Resolves the member in the specified solution snapshot. + /// Returns null if the member could not be resolved. + /// + IMember Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot); + } +} diff --git a/src/Main/Base/Project/Dom/IModelCollection.cs b/src/Main/Base/Project/Dom/IModelCollection.cs index 3f251e7bab..b5ce0e7f66 100644 --- a/src/Main/Base/Project/Dom/IModelCollection.cs +++ b/src/Main/Base/Project/Dom/IModelCollection.cs @@ -10,7 +10,7 @@ namespace ICSharpCode.SharpDevelop.Dom /// /// A read-only collection that provides change notifications. /// - public interface IModelCollection : IReadOnlyCollection, INotifyCollectionChanged + public interface IModelCollection : IReadOnlyList, INotifyCollectionChanged { } } diff --git a/src/Main/Base/Project/Dom/IModelService.cs b/src/Main/Base/Project/Dom/IModelService.cs new file mode 100644 index 0000000000..5a9e125bb9 --- /dev/null +++ b/src/Main/Base/Project/Dom/IModelService.cs @@ -0,0 +1,15 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// Service that enables lookup of model objects from NRefactory objects. + /// + public interface IModelService + { + } +} diff --git a/src/Main/Base/Project/Dom/ITypeDefinitionModel.cs b/src/Main/Base/Project/Dom/ITypeDefinitionModel.cs new file mode 100644 index 0000000000..c0e10383ba --- /dev/null +++ b/src/Main/Base/Project/Dom/ITypeDefinitionModel.cs @@ -0,0 +1,31 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// Observable model for a type definition. + /// + public interface ITypeDefinitionModel : IEntityModel + { + FullTypeName FullTypeName { get; } + IModelCollection NestedTypes { get; } + IModelCollection Members { get; } + + /// + /// Resolves the type definition in the current solution snapshot. + /// Returns null if the type definition could not be resolved. + /// + new ITypeDefinition Resolve(); + + /// + /// Resolves the type definition in the specified solution snapshot. + /// Returns null if the type definition could not be resolved. + /// + new ITypeDefinition Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot); + } +} diff --git a/src/Main/Base/Project/Dom/ITypeDefinitionModelCollection.cs b/src/Main/Base/Project/Dom/ITypeDefinitionModelCollection.cs new file mode 100644 index 0000000000..4b4dba61d2 --- /dev/null +++ b/src/Main/Base/Project/Dom/ITypeDefinitionModelCollection.cs @@ -0,0 +1,69 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using ICSharpCode.NRefactory.TypeSystem; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// A collection of type definition models with the ability to look them up by name. + /// + public interface ITypeDefinitionModelCollection : IModelCollection + { + /// + /// Gets the type definition model with the specified type name. + /// Returns null if no such model object exists. + /// + ITypeDefinitionModel this[FullTypeName fullTypeName] { get; } + + /// + /// Gets the type definition model with the specified type name. + /// Returns null if no such model object exists. + /// + ITypeDefinitionModel this[TopLevelTypeName topLevelTypeName] { get; } + } + + public sealed class EmptyTypeDefinitionModelCollection : ITypeDefinitionModelCollection + { + public static readonly EmptyTypeDefinitionModelCollection Instance = new EmptyTypeDefinitionModelCollection(); + + event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged { + add { } + remove { } + } + + ITypeDefinitionModel ITypeDefinitionModelCollection.this[FullTypeName name] { + get { return null; } + } + + ITypeDefinitionModel ITypeDefinitionModelCollection.this[TopLevelTypeName name] { + get { return null; } + } + + ITypeDefinitionModel IReadOnlyList.this[int index] { + get { + throw new ArgumentOutOfRangeException(); + } + } + + int IReadOnlyCollection.Count { + get { return 0; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Enumerable.Empty().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Enumerable.Empty().GetEnumerator(); + } + } +} diff --git a/src/Main/Base/Project/Dom/KeyedModelCollection.cs b/src/Main/Base/Project/Dom/KeyedModelCollection.cs new file mode 100644 index 0000000000..6aa1a0e42c --- /dev/null +++ b/src/Main/Base/Project/Dom/KeyedModelCollection.cs @@ -0,0 +1,47 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// Observable KeyedCollection. + /// + public abstract class KeyedModelCollection : KeyedCollection, IModelCollection + { + protected override void ClearItems() + { + base.ClearItems(); + if (CollectionChanged != null) + CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override void InsertItem(int index, TItem item) + { + base.InsertItem(index, item); + if (CollectionChanged != null) + CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + } + + protected override void RemoveItem(int index) + { + var oldItem = Items[index]; + base.RemoveItem(index); + if (CollectionChanged != null) + CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, index)); + } + + protected override void SetItem(int index, TItem item) + { + var oldItem = Items[index]; + base.SetItem(index, item); + if (CollectionChanged != null) + CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, index)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } +} diff --git a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj index e7a69097ee..afb123a8ae 100644 --- a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj +++ b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj @@ -95,6 +95,14 @@ Src\Project\MSBuildEngine\ExtendedBinaryReader.cs + + + + + + + + diff --git a/src/Main/Base/Project/Src/Project/AbstractProject.cs b/src/Main/Base/Project/Src/Project/AbstractProject.cs index 0ead8b0cd9..516169fce7 100644 --- a/src/Main/Base/Project/Src/Project/AbstractProject.cs +++ b/src/Main/Base/Project/Src/Project/AbstractProject.cs @@ -11,10 +11,10 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; - using ICSharpCode.Core; using ICSharpCode.NRefactory.TypeSystem; using ICSharpCode.SharpDevelop.Debugging; +using ICSharpCode.SharpDevelop.Dom; using ICSharpCode.SharpDevelop.Gui; using ICSharpCode.SharpDevelop.Gui.OptionPanels; using ICSharpCode.SharpDevelop.Parser; @@ -671,5 +671,11 @@ namespace ICSharpCode.SharpDevelop.Project return false; } } + + public virtual ICSharpCode.SharpDevelop.Dom.ITypeDefinitionModelCollection TypeDefinitionModels { + get { + return EmptyTypeDefinitionModelCollection.Instance; + } + } } } diff --git a/src/Main/Base/Project/Src/Project/IProject.cs b/src/Main/Base/Project/Src/Project/IProject.cs index 894191c17a..b19dfa3d3c 100644 --- a/src/Main/Base/Project/Src/Project/IProject.cs +++ b/src/Main/Base/Project/Src/Project/IProject.cs @@ -9,8 +9,10 @@ using System.ComponentModel; using System.IO; using System.Xml; using System.Xml.Linq; + using ICSharpCode.Core; using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Dom; using ICSharpCode.SharpDevelop.Gui; using ICSharpCode.SharpDevelop.Parser; @@ -319,6 +321,12 @@ namespace ICSharpCode.SharpDevelop.Project /// This method is called by the parser service within a per-file lock. /// void OnParseInformationUpdated(ParseInformationEventArgs args); + + /// + /// Gets the models for the top-level type definitions in this project. + /// Never returns null, but may return a permanently empty collection if this project does not support such models. + /// + ITypeDefinitionModelCollection TypeDefinitionModels { get; } } /// diff --git a/src/Main/Base/Test/Dom/CSharpModelTests.cs b/src/Main/Base/Test/Dom/CSharpModelTests.cs new file mode 100644 index 0000000000..a40290e965 --- /dev/null +++ b/src/Main/Base/Test/Dom/CSharpModelTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.CSharp; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; +using ICSharpCode.SharpDevelop.Project; +using NUnit.Framework; +using Rhino.Mocks; + +namespace ICSharpCode.SharpDevelop.Dom +{ + [TestFixture] + public class CSharpModelTests + { + IProject project; + IProjectContent projectContent; + IEntityModelContext context; + TopLevelTypeDefinitionModelCollection topLevelTypeModels; + + #region SetUp and other helper methods + [SetUp] + public virtual void SetUp() + { + SD.InitializeForUnitTests(); + SD.Services.AddStrictMockService(); + project = MockRepository.GenerateStrictMock(); + projectContent = new CSharpProjectContent().AddAssemblyReferences(AssemblyLoader.Corlib); + context = new ProjectEntityModelContext(project, ".cs"); + topLevelTypeModels = new TopLevelTypeDefinitionModelCollection(context); + + SD.ParserService.Stub(p => p.GetCompilation(project)).WhenCalled(c => c.ReturnValue = projectContent.CreateCompilation()); + } + + [TearDown] + public virtual void TearDown() + { + SD.TearDownForUnitTests(); + } + + protected void AddCodeFile(string fileName, string code) + { + var oldFile = projectContent.GetFile(fileName); + Assert.IsNull(oldFile); + var newFile = Parse(fileName, code); + projectContent = projectContent.AddOrUpdateFiles(newFile); + topLevelTypeModels.NotifyParseInformationChanged(oldFile, newFile); + } + + IUnresolvedFile Parse(string fileName, string code) + { + var parser = new CSharpParser(); + var syntaxTree = parser.Parse(code, fileName); + Assert.IsFalse(parser.HasErrors); + return syntaxTree.ToTypeSystem(); + } + + protected void UpdateCodeFile(string fileName, string code) + { + var oldFile = projectContent.GetFile(fileName); + Assert.IsNotNull(oldFile); + var newFile = Parse(fileName, code); + projectContent = projectContent.AddOrUpdateFiles(newFile); + topLevelTypeModels.NotifyParseInformationChanged(oldFile, newFile); + } + + protected void RemoveCodeFile(string fileName) + { + var oldFile = projectContent.GetFile(fileName); + projectContent = projectContent.RemoveFiles(fileName); + topLevelTypeModels.NotifyParseInformationChanged(oldFile, null); + } + #endregion + + [Test] + public void EmptyProject() + { + Assert.AreEqual(0, topLevelTypeModels.Count); + } + } +} diff --git a/src/Main/Base/Test/Dom/ProjectEntityModelContextTests.cs b/src/Main/Base/Test/Dom/ProjectEntityModelContextTests.cs new file mode 100644 index 0000000000..435e8b48e7 --- /dev/null +++ b/src/Main/Base/Test/Dom/ProjectEntityModelContextTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Project; +using NUnit.Framework; +using Rhino.Mocks; + +namespace ICSharpCode.SharpDevelop.Dom +{ + [TestFixture] + public class ProjectEntityModelContextTests + { + IUnresolvedTypeDefinition CreateMockTypeDefinition(string fileName) + { + var typeDefinition = MockRepository.GenerateStrictMock(); + var file = MockRepository.GenerateStrictMock(); + file.Stub(f => f.FileName).Return(fileName); + typeDefinition.Stub(td => td.UnresolvedFile).Return(file); + return typeDefinition; + } + + [Test] + public void XamlCodeBehindIsBetterThanXaml() + { + var context = new ProjectEntityModelContext(MockRepository.GenerateStrictMock(), ".cs"); + Assert.IsTrue(context.IsBetterPart(CreateMockTypeDefinition("Window.xaml.cs"), CreateMockTypeDefinition("Window.xaml"))); + Assert.IsTrue(context.IsBetterPart(CreateMockTypeDefinition("Window.cs"), CreateMockTypeDefinition("Window.xaml"))); + Assert.IsFalse(context.IsBetterPart(CreateMockTypeDefinition("Window.xaml"), CreateMockTypeDefinition("Window.xaml.cs"))); + Assert.IsFalse(context.IsBetterPart(CreateMockTypeDefinition("Window.xaml"), CreateMockTypeDefinition("Window.cs"))); + } + + [Test] + public void MainPartIsBetterThanDesigner() + { + var context = new ProjectEntityModelContext(MockRepository.GenerateStrictMock(), ".cs"); + Assert.IsTrue(context.IsBetterPart(CreateMockTypeDefinition("Form.cs"), CreateMockTypeDefinition("Form.Designer.cs"))); + Assert.IsFalse(context.IsBetterPart(CreateMockTypeDefinition("Form.Designer.cs"), CreateMockTypeDefinition("Form.cs"))); + } + } +} diff --git a/src/Main/Base/Test/ICSharpCode.SharpDevelop.Tests.csproj b/src/Main/Base/Test/ICSharpCode.SharpDevelop.Tests.csproj index 6f926f9d76..c516858e55 100644 --- a/src/Main/Base/Test/ICSharpCode.SharpDevelop.Tests.csproj +++ b/src/Main/Base/Test/ICSharpCode.SharpDevelop.Tests.csproj @@ -78,6 +78,8 @@ + + @@ -114,6 +116,7 @@ + @@ -160,6 +163,10 @@ {6C55B776-26D4-4DB3-A6AB-87E783B2F3D1} ICSharpCode.AvalonEdit + + {53DCA265-3C3C-42F9-B647-F72BA678122B} + ICSharpCode.NRefactory.CSharp + {3B2A5653-EC97-4001-BB9B-D90F1AF2C371} ICSharpCode.NRefactory diff --git a/src/Main/Base/Test/Utils/AssemblyLoader.cs b/src/Main/Base/Test/Utils/AssemblyLoader.cs new file mode 100644 index 0000000000..e2f528c7cd --- /dev/null +++ b/src/Main/Base/Test/Utils/AssemblyLoader.cs @@ -0,0 +1,13 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using ICSharpCode.NRefactory.TypeSystem; + +namespace ICSharpCode.SharpDevelop +{ + public static class AssemblyLoader + { + public static readonly IUnresolvedAssembly Corlib = new CecilLoader { LazyLoad = true }.LoadAssemblyFile(typeof(object).Assembly.Location); + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.Widgets/Project/MyersDiff/MyersDiffAlgorithm.cs b/src/Main/ICSharpCode.SharpDevelop.Widgets/Project/MyersDiff/MyersDiffAlgorithm.cs index f9d50a5b6b..2a47d896d4 100644 --- a/src/Main/ICSharpCode.SharpDevelop.Widgets/Project/MyersDiff/MyersDiffAlgorithm.cs +++ b/src/Main/ICSharpCode.SharpDevelop.Widgets/Project/MyersDiff/MyersDiffAlgorithm.cs @@ -59,11 +59,11 @@ namespace ICSharpCode.SharpDevelop.Widgets.MyersDiff /// /// Example: /// - /// H E L L O W O R L D - /// ____ - /// L \___ - /// O \___ - /// W \________ + /// H E L L O W O R L D + /// ____ + /// L \___ + /// O \___ + /// W \________ /// /// Since every D-path has exactly D horizontal or vertical elements, it can /// only end on the diagonals -D, -D+2, ..., D-2, D. diff --git a/src/Main/SharpDevelop/Dom/MemberModel.cs b/src/Main/SharpDevelop/Dom/MemberModel.cs new file mode 100644 index 0000000000..70e3c51e14 --- /dev/null +++ b/src/Main/SharpDevelop/Dom/MemberModel.cs @@ -0,0 +1,105 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; +using ICSharpCode.SharpDevelop.Project; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// A mutable class that can track a member as the solution is being changed. + /// + sealed class MemberModel : IMemberModel + { + /// + /// A strong reference to the parent TypeDefinitionModel.Members collection. + /// This is necessary to prevent the garbage collector from + /// freeing the weak reference to the collection while one of the member models + /// is still in use. + /// If we don't prevent this, the type definition model might create multiple + /// MemberModels for the same member. + /// + internal IModelCollection strongParentCollectionReference; + + readonly IEntityModelContext context; + IUnresolvedMember member; + + public MemberModel(IEntityModelContext context, IUnresolvedMember member) + { + if (context == null) + throw new ArgumentNullException("context"); + if (member == null) + throw new ArgumentNullException("member"); + this.context = context; + this.member = member; + } + + public event PropertyChangedEventHandler PropertyChanged { add {} remove {} } + + public void Update(IUnresolvedMember newMember) + { + if (newMember == null) + throw new ArgumentNullException("newMember"); + this.member = newMember; + } + + public IProject ParentProject { + get { return context.Project; } + } + + public IUnresolvedMember UnresolvedMember { + get { return member; } + } + + public EntityType EntityType { + get { return member.EntityType; } + } + + public DomRegion Region { + get { return member.Region; } + } + + public string Name { + get { return member.Name; } + } + + /// + /// Gets the full type name of the type that declares this member. + /// + public FullTypeName DeclaringTypeName { + get { + if (member.DeclaringTypeDefinition != null) + return member.DeclaringTypeDefinition.FullTypeName; + else + return new TopLevelTypeName(string.Empty, string.Empty); + } + } + + #region Resolve + public IMember Resolve() + { + var compilation = context.GetCompilation(null); + return member.Resolve(new SimpleTypeResolveContext(compilation.MainAssembly)); + } + + public IMember Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + var compilation = context.GetCompilation(solutionSnapshot); + return member.Resolve(new SimpleTypeResolveContext(compilation.MainAssembly)); + } + + IEntity IEntityModel.Resolve() + { + return Resolve(); + } + + IEntity IEntityModel.Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + return Resolve(solutionSnapshot); + } + #endregion + } +} diff --git a/src/Main/SharpDevelop/Dom/TopLevelTypeDefinitionModelCollection.cs b/src/Main/SharpDevelop/Dom/TopLevelTypeDefinitionModelCollection.cs new file mode 100644 index 0000000000..06598219cf --- /dev/null +++ b/src/Main/SharpDevelop/Dom/TopLevelTypeDefinitionModelCollection.cs @@ -0,0 +1,52 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.Specialized; +using ICSharpCode.NRefactory.TypeSystem; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// A TypeDefinitionModel-collection that holds models for all top-level types in a project content. + /// + sealed class TopLevelTypeDefinitionModelCollection : KeyedModelCollection + { + readonly IEntityModelContext context; + + public TopLevelTypeDefinitionModelCollection(IEntityModelContext context) + { + if (context == null) + throw new ArgumentNullException("context"); + this.context = context; + } + + public TypeDefinitionModel this[FullTypeName fullTypeName] { + get { + TypeDefinitionModel model = base[fullTypeName.TopLevelTypeName]; + for (int i = 0; i < fullTypeName.NestingLevel; i++) { + throw new NotImplementedException(); + } + return model; + } + } + + /// + /// Updates the parse information. + /// + public void NotifyParseInformationChanged(IUnresolvedFile oldFile, IUnresolvedFile newFile) + { + if (oldFile != null) { + + } + } + + protected override TopLevelTypeName GetKeyForItem(TypeDefinitionModel item) + { + return item.FullTypeName.TopLevelTypeName; + } + } +} diff --git a/src/Main/SharpDevelop/Dom/TypeDefinitionModel.cs b/src/Main/SharpDevelop/Dom/TypeDefinitionModel.cs new file mode 100644 index 0000000000..b4ac2e66a5 --- /dev/null +++ b/src/Main/SharpDevelop/Dom/TypeDefinitionModel.cs @@ -0,0 +1,264 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; + +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.SharpDevelop.Parser; +using ICSharpCode.SharpDevelop.Project; + +namespace ICSharpCode.SharpDevelop.Dom +{ + /// + /// A mutable class that can track a type definition as the solution is being changed. + /// + sealed class TypeDefinitionModel : ITypeDefinitionModel + { + readonly IEntityModelContext context; + readonly FullTypeName fullTypeName; + List parts; + + public TypeDefinitionModel(IEntityModelContext context, params IUnresolvedTypeDefinition[] parts) + { + if (context == null) + throw new ArgumentNullException("context"); + if (parts.Length == 0) + throw new ArgumentException("Number of parts must not be zero"); + this.context = context; + this.parts = new List(parts); + MovePrimaryPartToFront(); + this.fullTypeName = parts[0].FullTypeName; + } + + void MovePrimaryPartToFront() + { + int bestPartIndex = 0; + for (int i = 1; i < parts.Count; i++) { + if (context.IsBetterPart(parts[i], parts[bestPartIndex])) + bestPartIndex = i; + } + IUnresolvedTypeDefinition primaryPart = parts[bestPartIndex]; + for (int i = bestPartIndex; i > 0; i--) { + parts[i] = parts[i - 1]; + } + parts[0] = primaryPart; + } + + /* + /// + /// Updates the type definition model by removing the old parts and adding the new ones. + /// + public void Update(IReadOnlyList removedParts, IReadOnlyList newParts) + { + SD.MainThread.VerifyAccess(); + if (removedParts != null) + foreach (var p in removedParts) + parts.Remove(p); + if (newParts != null) + parts.AddRange(newParts); + + MemberModelCollection members; + if (membersWeakReference.TryGetTarget(out members)) { + members.Update(parts); + } + }*/ + + public IProject ParentProject { + get { return context.Project; } + } + + public FullTypeName FullTypeName { + get { return fullTypeName; } + } + + public DomRegion Region { + get { return parts[0].Region; } + } + + public string Name { + get { return fullTypeName.Name; } + } + + #region Resolve + public ITypeDefinition Resolve() + { + var compilation = context.GetCompilation(null); + return compilation.MainAssembly.GetTypeDefinition(fullTypeName); + } + + public ITypeDefinition Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + var compilation = context.GetCompilation(solutionSnapshot); + return compilation.MainAssembly.GetTypeDefinition(fullTypeName); + } + + IEntity IEntityModel.Resolve() + { + return Resolve(); + } + + IEntity IEntityModel.Resolve(ISolutionSnapshotWithProjectMapping solutionSnapshot) + { + return Resolve(solutionSnapshot); + } + #endregion + + public event PropertyChangedEventHandler PropertyChanged { add {} remove {} } + + #region Members collection + sealed class MemberCollection : IModelCollection + { + readonly TypeDefinitionModel parent; + List> lists = new List>(); + + public MemberCollection(TypeDefinitionModel parent) + { + this.parent = parent; + } + + public void Insert(int partIndex, IUnresolvedTypeDefinition newPart) + { + List newItems = new List(newPart.Members.Count); + foreach (var newMember in newPart.Members) { + newItems.Add(new MemberModel(parent.context, newMember) { strongParentCollectionReference = this }); + } + lists.Insert(partIndex, newItems); + if (collectionChanged != null) + collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, GetCount(partIndex))); + } + + public void Remove(int partIndex) + { + var oldItems = lists[partIndex]; + lists.RemoveAt(partIndex); + if (collectionChanged != null) + collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems, GetCount(partIndex))); + } + + public void Update(int partIndex, IUnresolvedTypeDefinition newPart) + { + List list = lists[partIndex]; + var newMembers = newPart.Members; + int startPos = 0; + // Look at the initial members and update them if they're matching + while (startPos < list.Count && startPos < newMembers.Count && IsMatch(list[startPos], newMembers[startPos])) { + list[startPos].Update(newMembers[startPos]); + startPos++; + } + // Look at the final members + int endPosOld = list.Count - 1; + int endPosNew = newMembers.Count - 1; + while (endPosOld >= startPos && endPosNew >= startPos && IsMatch(list[endPosOld], newMembers[endPosNew])) { + list[endPosOld--].Update(newMembers[endPosNew--]); + } + // [startPos, endPos] is the middle portion that contains all the changes + // Add one to endPos so that it's the exclusive end of the middle portion: + endPosOld++; + endPosNew++; + // [startPos, endPos) + + // Now we still need to update the members in between. + // We might try to be clever here and find a LCS so that we only update the members that were actually changed, + // or we might consider moving members around (INotifyCollectionChanged supports moves) + // However, the easiest solution by far is to just remove + readd the whole middle portion. + var oldItems = collectionChanged != null ? list.GetRange(startPos, endPosOld - startPos) : null; + list.RemoveRange(startPos, endPosOld - startPos); + var newItems = new MemberModel[endPosNew - startPos]; + for (int i = 0; i < newItems.Length; i++) { + newItems[i] = new MemberModel(parent.context, newMembers[startPos + i]); + newItems[i].strongParentCollectionReference = this; + } + list.InsertRange(startPos, newItems); + if (collectionChanged != null) + collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, GetCount(partIndex) + startPos)); + } + + static bool IsMatch(MemberModel memberModel, IUnresolvedMember newMember) + { + return memberModel.EntityType == newMember.EntityType && memberModel.Name == newMember.Name; + } + + NotifyCollectionChangedEventHandler collectionChanged; + + public event NotifyCollectionChangedEventHandler CollectionChanged { + add { + collectionChanged += value; + // Set strong reference to collection while there are event listeners + if (collectionChanged != null) + parent.membersStrongReference = this; + } + remove { + collectionChanged -= value; + if (collectionChanged == null) + parent.membersStrongReference = null; + } + } + + int GetCount(int partIndex) + { + int count = 0; + for (int i = 0; i < partIndex; i++) { + count += lists[i].Count; + } + return count; + } + + public int Count { + get { return GetCount(lists.Count); } + } + + public IEnumerator GetEnumerator() + { + return lists.SelectMany(i => i).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public MemberModel this[int index] { + get { + int inputIndex = 0; + while (index >= lists[inputIndex].Count) { + index -= lists[inputIndex].Count; + inputIndex++; + } + return lists[inputIndex][index]; + } + } + } + + WeakReference membersWeakReference = new WeakReference(null); + + /// + /// Used to keep the member model collection alive while there are event listeners. + /// This is necessary to prevent us from creating multiple member models for the same member. + /// + MemberCollection membersStrongReference; + + public IModelCollection Members { + get { + SD.MainThread.VerifyAccess(); + MemberCollection members; + if (!membersWeakReference.TryGetTarget(out members)) { + members = new MemberCollection(this); + membersWeakReference.SetTarget(members); + } + return members; + } + } + #endregion + + public IModelCollection NestedTypes { + get { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Main/SharpDevelop/SharpDevelop.csproj b/src/Main/SharpDevelop/SharpDevelop.csproj index 683b7ca4a0..8bcc91b28c 100644 --- a/src/Main/SharpDevelop/SharpDevelop.csproj +++ b/src/Main/SharpDevelop/SharpDevelop.csproj @@ -81,7 +81,10 @@ + + + LoadSaveOptions.xaml Code