diff --git a/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlParsedFile.cs b/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlParsedFile.cs index adf7f06a10..d136605e2e 100644 --- a/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlParsedFile.cs +++ b/src/AddIns/BackendBindings/XamlBinding/XamlBinding/XamlParsedFile.cs @@ -53,10 +53,11 @@ namespace ICSharpCode.XamlBinding get { return fileName; } } - DateTime lastWriteTime = DateTime.UtcNow; + DateTime? lastWriteTime; - public DateTime LastWriteTime { + public DateTime? LastWriteTime { get { return lastWriteTime; } + set { lastWriteTime = value; } } public IList TopLevelTypeDefinitions { diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory.CSharp/TypeSystem/CSharpParsedFile.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory.CSharp/TypeSystem/CSharpParsedFile.cs index b2b73947c7..889fde118a 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory.CSharp/TypeSystem/CSharpParsedFile.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory.CSharp/TypeSystem/CSharpParsedFile.cs @@ -73,9 +73,9 @@ namespace ICSharpCode.NRefactory.CSharp.TypeSystem get { return fileName; } } - DateTime lastWriteTime = DateTime.UtcNow; + DateTime? lastWriteTime; - public DateTime LastWriteTime { + public DateTime? LastWriteTime { get { return lastWriteTime; } set { FreezableHelper.ThrowIfFrozen(this); diff --git a/src/Libraries/NRefactory/ICSharpCode.NRefactory/TypeSystem/IParsedFile.cs b/src/Libraries/NRefactory/ICSharpCode.NRefactory/TypeSystem/IParsedFile.cs index 07863c799f..0a9a04f83e 100644 --- a/src/Libraries/NRefactory/ICSharpCode.NRefactory/TypeSystem/IParsedFile.cs +++ b/src/Libraries/NRefactory/ICSharpCode.NRefactory/TypeSystem/IParsedFile.cs @@ -34,7 +34,7 @@ namespace ICSharpCode.NRefactory.TypeSystem /// /// Gets the time when the file was last written. /// - DateTime LastWriteTime { get; } + DateTime? LastWriteTime { get; set; } /// /// Gets all top-level type definitions. diff --git a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj index 8e1e40bac0..63db9a9579 100644 --- a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj +++ b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj @@ -357,6 +357,7 @@ + diff --git a/src/Main/Base/Project/Src/Project/AbstractProject.cs b/src/Main/Base/Project/Src/Project/AbstractProject.cs index 029ca9f3fa..8dd5391753 100644 --- a/src/Main/Base/Project/Src/Project/AbstractProject.cs +++ b/src/Main/Base/Project/Src/Project/AbstractProject.cs @@ -572,8 +572,17 @@ namespace ICSharpCode.SharpDevelop.Project return false; } + Properties projectSpecificProperties = new Properties(); + [Browsable(false)] - public Properties ProjectSpecificProperties { get; protected set; } + public Properties ProjectSpecificProperties { + get { return projectSpecificProperties; } + set { + if (value == null) + throw new ArgumentNullException(); + projectSpecificProperties = value; + } + } public virtual string GetDefaultNamespace(string fileName) { diff --git a/src/Main/Base/Project/Src/Services/File/OnDiskTextSourceVersion.cs b/src/Main/Base/Project/Src/Services/File/OnDiskTextSourceVersion.cs new file mode 100644 index 0000000000..eaddb3e00c --- /dev/null +++ b/src/Main/Base/Project/Src/Services/File/OnDiskTextSourceVersion.cs @@ -0,0 +1,49 @@ +// 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.Generic; +using ICSharpCode.NRefactory; +using ICSharpCode.NRefactory.Editor; + +namespace ICSharpCode.SharpDevelop +{ + /// + /// Signifies that the text source matches the version of the file on disk. + /// + public sealed class OnDiskTextSourceVersion : ITextSourceVersion + { + public readonly DateTime LastWriteTime; + + public OnDiskTextSourceVersion(DateTime lastWriteTime) + { + this.LastWriteTime = lastWriteTime; + } + + public bool BelongsToSameDocumentAs(ITextSourceVersion other) + { + return this == other; + } + + public int CompareAge(ITextSourceVersion other) + { + if (this != other) + throw new ArgumentException("other belongs to different document"); + return 0; + } + + public IEnumerable GetChangesTo(ITextSourceVersion other) + { + if (this != other) + throw new ArgumentException("other belongs to different document"); + return EmptyList.Instance; + } + + public int MoveOffsetTo(ITextSourceVersion other, int oldOffset, AnchorMovementType movement) + { + if (this != other) + throw new ArgumentException("other belongs to different document"); + return oldOffset; + } + } +} diff --git a/src/Main/Base/Project/Src/Services/ParserService/IParserService.cs b/src/Main/Base/Project/Src/Services/ParserService/IParserService.cs index 5b811be4e4..547bf46210 100644 --- a/src/Main/Base/Project/Src/Services/ParserService/IParserService.cs +++ b/src/Main/Base/Project/Src/Services/ParserService/IParserService.cs @@ -269,6 +269,13 @@ namespace ICSharpCode.SharpDevelop.Parser /// event EventHandler ParseInformationUpdated; #endregion + + /// + /// Registers parse information for the specified file. + /// The file must belong to the specified project, otherwise this method does nothing. + /// + /// This method is intended for restoring parse information cached on disk. + void RegisterParsedFile(FileName fileName, IProject project, IParsedFile parsedFile); } public interface ILoadSolutionProjectsThread diff --git a/src/Main/Base/Project/Src/Services/ParserService/ParseProjectContent.cs b/src/Main/Base/Project/Src/Services/ParserService/ParseProjectContent.cs index 3ba5210d74..99587c4697 100644 --- a/src/Main/Base/Project/Src/Services/ParserService/ParseProjectContent.cs +++ b/src/Main/Base/Project/Src/Services/ParserService/ParseProjectContent.cs @@ -5,12 +5,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; + using ICSharpCode.Core; using ICSharpCode.NRefactory.Editor; using ICSharpCode.NRefactory.TypeSystem; using ICSharpCode.NRefactory.TypeSystem.Implementation; +using ICSharpCode.NRefactory.Utils; using ICSharpCode.SharpDevelop.Gui; using ICSharpCode.SharpDevelop.Project; @@ -33,6 +36,8 @@ namespace ICSharpCode.SharpDevelop.Parser // time necessary for loading references, in relation to time for a single C# file const int LoadingReferencesWorkAmount = 15; + string cacheFileName; + public ParseProjectContentContainer(MSBuildBasedProject project, IProjectContent initialProjectContent) { if (project == null) @@ -40,6 +45,8 @@ namespace ICSharpCode.SharpDevelop.Parser this.project = project; this.projectContent = initialProjectContent.SetAssemblyName(project.AssemblyName); + this.cacheFileName = GetCacheFileName(FileName.Create(project.FileName)); + ProjectService.ProjectItemAdded += OnProjectItemAdded; ProjectService.ProjectItemRemoved += OnProjectItemRemoved; @@ -70,8 +77,104 @@ namespace ICSharpCode.SharpDevelop.Parser foreach (var parsedFile in projectContent.Files) { SD.ParserService.RemoveOwnerProject(FileName.Create(parsedFile.FileName), project); } + var pc = projectContent; + Task.Run( + delegate { + pc = pc.RemoveAssemblyReferences(pc.AssemblyReferences); + int serializableFileCount = 0; + List nonSerializableParsedFiles = new List(); + foreach (var parsedFile in pc.Files) { + if (!parsedFile.GetType().IsSerializable || parsedFile.LastWriteTime == default(DateTime)) + nonSerializableParsedFiles.Add(parsedFile); + else + serializableFileCount++; + } + // remove non-serializable parsed files + if (nonSerializableParsedFiles.Count > 0) + pc = pc.UpdateProjectContent(nonSerializableParsedFiles, null); + if (serializableFileCount > 3) + SaveToCache(cacheFileName, pc); + else + RemoveCache(cacheFileName); + }).FireAndForget(); + } + + #region Caching logic (serialization) + + static string GetCacheFileName(FileName projectFileName) + { + string persistencePath = SD.AssemblyParserService.DomPersistencePath; + if (persistencePath == null) + return null; + string cacheFileName = Path.GetFileNameWithoutExtension(projectFileName); + if (cacheFileName.Length > 32) + cacheFileName = cacheFileName.Substring(cacheFileName.Length - 32); // use 32 last characters + cacheFileName = Path.Combine(persistencePath, cacheFileName + "." + projectFileName.GetHashCode().ToString("x8") + ".prj"); + return cacheFileName; + } + + static IProjectContent TryReadFromCache(string cacheFileName) + { + if (cacheFileName == null || !File.Exists(cacheFileName)) + return null; + LoggingService.Debug("Deserializing " + cacheFileName); + try { + using (FileStream fs = new FileStream(cacheFileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, 4096, FileOptions.SequentialScan)) { + using (BinaryReader reader = new BinaryReaderWith7BitEncodedInts(fs)) { + FastSerializer s = new FastSerializer(); + return (IProjectContent)s.Deserialize(reader); + } + } + } catch (IOException ex) { + LoggingService.Warn(ex); + return null; + } catch (UnauthorizedAccessException ex) { + LoggingService.Warn(ex); + return null; + } catch (SerializationException ex) { + LoggingService.Warn(ex); + return null; + } } + static void SaveToCache(string cacheFileName, IProjectContent pc) + { + if (cacheFileName == null) + return; + LoggingService.Debug("Serializing to " + cacheFileName); + try { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFileName)); + using (FileStream fs = new FileStream(cacheFileName, FileMode.Create, FileAccess.Write)) { + using (BinaryWriter writer = new BinaryWriterWith7BitEncodedInts(fs)) { + FastSerializer s = new FastSerializer(); + s.Serialize(writer, pc); + } + } + } catch (IOException ex) { + LoggingService.Warn(ex); + // Can happen if two SD instances are trying to access the file at the same time. + // We'll just let one of them win, and instance that got the exception won't write to the cache at all. + // Similarly, we also ignore the other kinds of IO exceptions. + } catch (UnauthorizedAccessException ex) { + LoggingService.Warn(ex); + } + } + + void RemoveCache(string cacheFileName) + { + if (cacheFileName == null) + return; + try { + File.Delete(cacheFileName); + } catch (IOException ex) { + LoggingService.Warn(ex); + } catch (UnauthorizedAccessException ex) { + LoggingService.Warn(ex); + } + } + + #endregion + public IProjectContent ProjectContent { get { lock (lockObj) { @@ -120,6 +223,7 @@ namespace ICSharpCode.SharpDevelop.Parser void ParseFiles(IReadOnlyList filesToParse, IProgressMonitor progressMonitor) { + IProjectContent cachedPC = TryReadFromCache(cacheFileName); ParseableFileContentFinder finder = new ParseableFileContentFinder(); object progressLock = new object(); @@ -131,9 +235,26 @@ namespace ICSharpCode.SharpDevelop.Parser CancellationToken = progressMonitor.CancellationToken }, fileName => { - ITextSource content = finder.Create(fileName); - if (content != null) { - SD.ParserService.ParseFile(fileName, content, project); + ITextSource content = finder.CreateForOpenFile(fileName); + bool wasLoadedFromCache = false; + if (content == null && cachedPC != null) { + IParsedFile parsedFile = cachedPC.GetFile(fileName); + if (parsedFile != null && parsedFile.LastWriteTime == File.GetLastWriteTimeUtc(fileName)) { + SD.ParserService.RegisterParsedFile(fileName, project, parsedFile); + wasLoadedFromCache = true; + } + } + if (!wasLoadedFromCache) { + if (content == null) { + try { + content = SD.FileService.GetFileContentFromDisk(fileName); + } catch (IOException) { + } catch (UnauthorizedAccessException) { + } + } + if (content != null) { + SD.ParserService.ParseFile(fileName, content, project); + } } lock (progressLock) { progressMonitor.Progress += fileCountInverse; diff --git a/src/Main/Base/Project/Src/Services/ProjectService/ParseableFileContentFinder.cs b/src/Main/Base/Project/Src/Services/ProjectService/ParseableFileContentFinder.cs index 42bc638f07..498b6f3ecd 100644 --- a/src/Main/Base/Project/Src/Services/ProjectService/ParseableFileContentFinder.cs +++ b/src/Main/Base/Project/Src/Services/ProjectService/ParseableFileContentFinder.cs @@ -19,17 +19,25 @@ namespace ICSharpCode.SharpDevelop.Project { FileName[] viewContentFileNamesCollection = SD.MainThread.InvokeIfRequired(() => SD.FileService.OpenedFiles.Select(f => f.FileName).ToArray()); + public ITextSource CreateForOpenFile(FileName fileName) + { + foreach (FileName name in viewContentFileNamesCollection) { + if (FileUtility.IsEqualFileName(name, fileName)) + return SD.FileService.GetFileContentForOpenFile(fileName); + } + return null; + } + /// /// Retrieves the file contents for the specified project items. /// public ITextSource Create(FileName fileName) { - foreach (FileName name in viewContentFileNamesCollection) { - if (FileUtility.IsEqualFileName(name, fileName)) - return SD.FileService.GetFileContent(fileName); - } + ITextSource textSource = CreateForOpenFile(fileName); + if (textSource != null) + return textSource; try { - return new StringTextSource(ICSharpCode.AvalonEdit.Utils.FileReader.ReadFileContent(fileName, SD.FileService.DefaultFileEncoding)); + return SD.FileService.GetFileContentFromDisk(fileName); } catch (IOException) { return null; } catch (UnauthorizedAccessException) { diff --git a/src/Main/SharpDevelop/Parser/AssemblyParserService.cs b/src/Main/SharpDevelop/Parser/AssemblyParserService.cs index 7e99ff2e32..996a8dcabb 100644 --- a/src/Main/SharpDevelop/Parser/AssemblyParserService.cs +++ b/src/Main/SharpDevelop/Parser/AssemblyParserService.cs @@ -140,7 +140,7 @@ namespace ICSharpCode.SharpDevelop.Parser if (pc != null) return pc; - LoggingService.Debug("Loading " + fileName); + //LoggingService.Debug("Loading " + fileName); cancellationToken.ThrowIfCancellationRequested(); var param = new ReaderParameters(); param.AssemblyResolver = new DummyAssemblyResolver(); @@ -246,16 +246,15 @@ namespace ICSharpCode.SharpDevelop.Parser { if (cacheFileName == null || !File.Exists(cacheFileName)) return null; - LoggingService.Debug("Deserializing " + cacheFileName); + //LoggingService.Debug("Deserializing " + cacheFileName); try { using (FileStream fs = new FileStream(cacheFileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, 4096, FileOptions.SequentialScan)) { using (BinaryReader reader = new BinaryReaderWith7BitEncodedInts(fs)) { if (reader.ReadInt64() != lastWriteTime.Ticks) { - LoggingService.Debug("Timestamp mismatch, deserialization aborted."); + LoggingService.Debug("Timestamp mismatch, deserialization aborted. (" + cacheFileName + ")"); return null; } FastSerializer s = new FastSerializer(); - s.SerializationBinder = new MySerializationBinder(); return (IUnresolvedAssembly)s.Deserialize(reader); } } @@ -282,7 +281,6 @@ namespace ICSharpCode.SharpDevelop.Parser using (BinaryWriter writer = new BinaryWriterWith7BitEncodedInts(fs)) { writer.Write(lastWriteTime.Ticks); FastSerializer s = new FastSerializer(); - s.SerializationBinder = new MySerializationBinder(); s.Serialize(writer, pc); } } @@ -295,33 +293,6 @@ namespace ICSharpCode.SharpDevelop.Parser LoggingService.Warn(ex); } } - - sealed class MySerializationBinder : SerializationBinder - { - public override void BindToName(Type serializedType, out string assemblyName, out string typeName) - { - if (serializedType.Assembly == typeof(IProjectContent).Assembly) { - assemblyName = "NRefactory"; - } else { - assemblyName = serializedType.Assembly.FullName; - } - typeName = serializedType.FullName; - } - - public override Type BindToType(string assemblyName, string typeName) - { - Assembly asm; - switch (assemblyName) { - case "NRefactory": - asm = typeof(IProjectContent).Assembly; - break; - default: - asm = Assembly.Load(assemblyName); - break; - } - return asm.GetType(typeName); - } - } #endregion } } diff --git a/src/Main/SharpDevelop/Parser/ParserService.cs b/src/Main/SharpDevelop/Parser/ParserService.cs index 51b11b3792..e2120ac546 100644 --- a/src/Main/SharpDevelop/Parser/ParserService.cs +++ b/src/Main/SharpDevelop/Parser/ParserService.cs @@ -360,5 +360,10 @@ namespace ICSharpCode.SharpDevelop.Parser { // TODO } + + public void RegisterParsedFile(FileName fileName, IProject project, IParsedFile parsedFile) + { + GetFileEntry(fileName, true).RegisterParsedFile(project, parsedFile); + } } } diff --git a/src/Main/SharpDevelop/Parser/ParserServiceEntry.cs b/src/Main/SharpDevelop/Parser/ParserServiceEntry.cs index de1c7f94d6..1c6be6c166 100644 --- a/src/Main/SharpDevelop/Parser/ParserServiceEntry.cs +++ b/src/Main/SharpDevelop/Parser/ParserServiceEntry.cs @@ -221,6 +221,9 @@ namespace ICSharpCode.SharpDevelop.Parser throw new NullReferenceException(parser.GetType().Name + ".Parse() returned null"); if (fullParseInformationRequested && !parseInfo.IsFullParseInformation) throw new InvalidOperationException(parser.GetType().Name + ".Parse() did not return full parse info as requested."); + OnDiskTextSourceVersion onDiskVersion = fileContent.Version as OnDiskTextSourceVersion; + if (onDiskVersion != null) + parseInfo.ParsedFile.LastWriteTime = onDiskVersion.LastWriteTime; FreezableHelper.Freeze(parseInfo.ParsedFile); results[i] = new ParseInformationEventArgs(entries[i].Project, entries[i].ParsedFile, parseInfo); } @@ -326,5 +329,25 @@ namespace ICSharpCode.SharpDevelop.Parser return task; } #endregion + + public void RegisterParsedFile(IProject project, IParsedFile parsedFile) + { + if (project == null) + throw new ArgumentNullException("project"); + if (parsedFile == null) + throw new ArgumentNullException("parsedFile"); + FreezableHelper.Freeze(parsedFile); + var newParseInfo = new ParseInformation(parsedFile, false); + lock (this) { + int index = FindIndexForProject(project); + if (index >= 0) { + currentVersion = null; + var args = new ParseInformationEventArgs(project, entries[index].ParsedFile, newParseInfo); + entries[index] = new ProjectEntry(project, parsedFile, null); + project.OnParseInformationUpdated(args); + parserService.RaiseParseInformationUpdated(args); + } + } + } } } diff --git a/src/Main/SharpDevelop/SharpDevelop.csproj b/src/Main/SharpDevelop/SharpDevelop.csproj index 044d6680bb..e32b238416 100644 --- a/src/Main/SharpDevelop/SharpDevelop.csproj +++ b/src/Main/SharpDevelop/SharpDevelop.csproj @@ -138,6 +138,8 @@ + + {2FF700C2-A38A-48BD-A637-8CAFD4FE6237} AvalonDock diff --git a/src/Main/SharpDevelop/Workbench/FileService.cs b/src/Main/SharpDevelop/Workbench/FileService.cs index 8f0794d3fd..52b4ee1d68 100644 --- a/src/Main/SharpDevelop/Workbench/FileService.cs +++ b/src/Main/SharpDevelop/Workbench/FileService.cs @@ -118,7 +118,10 @@ namespace ICSharpCode.SharpDevelop.Workbench public ITextSource GetFileContentFromDisk(FileName fileName, CancellationToken cancellationToken) { - return new StringTextSource(FileReader.ReadFileContent(fileName, DefaultFileEncoding)); + cancellationToken.ThrowIfCancellationRequested(); + string text = FileReader.ReadFileContent(fileName, DefaultFileEncoding); + DateTime lastWriteTime = File.GetLastWriteTimeUtc(fileName); + return new StringTextSource(text, new OnDiskTextSourceVersion(lastWriteTime)); } #endregion