diff --git a/src/fsharp/fsi/console.fs b/src/fsharp/fsi/console.fs index 1ce4d942be3..54bcec08512 100644 --- a/src/fsharp/fsi/console.fs +++ b/src/fsharp/fsi/console.fs @@ -131,7 +131,7 @@ module Utils = type Cursor = static member ResetTo(top,left) = Utils.guard(fun () -> - Console.CursorTop <- top; + Console.CursorTop <- min top (Console.BufferHeight - 1); Console.CursorLeft <- left) static member Move(inset, delta) = let position = Console.CursorTop * (Console.BufferWidth - inset) + (Console.CursorLeft - inset) + delta @@ -216,6 +216,7 @@ type ReadLineConsole() = if currLeft < x.Inset then if currLeft = 0 then Console.Write (if prompt then x.Prompt2 else String(' ',x.Inset)) Utils.guard(fun () -> + Console.CursorTop <- min Console.CursorTop (Console.BufferHeight - 1); Console.CursorLeft <- x.Inset); // The caller writes the primary prompt. If we are reading the 2nd and subsequent lines of the diff --git a/vsintegration/src/deployment/EnableOpenSource/EnableOpenSource.pkgdef b/vsintegration/src/deployment/EnableOpenSource/EnableOpenSource.pkgdef index 9742b56fa0f..04f6584a0a4 100644 --- a/vsintegration/src/deployment/EnableOpenSource/EnableOpenSource.pkgdef +++ b/vsintegration/src/deployment/EnableOpenSource/EnableOpenSource.pkgdef @@ -23,7 +23,7 @@ "newVersion"="4.3.1.9055" "codeBase"="$PackageFolder$\FSharp.Core.dll" -[$RootKey$\RuntimeConfiguration\dependentAssembly\bindingRedirection\{2DB67780-3B09-41E5-A8DC-92AF2E1665BD}}] +[$RootKey$\RuntimeConfiguration\dependentAssembly\bindingRedirection\{2DB67780-3B09-41E5-A8DC-92AF2E1665BD}] "name"="FSharp.Editor" "publicKeyToken"="b03f5f7f11d50a3a" "culture"="neutral" diff --git a/vsintegration/src/unittests/Tests.ProjectSystem.UpToDate.fs b/vsintegration/src/unittests/Tests.ProjectSystem.UpToDate.fs new file mode 100644 index 00000000000..bcf5e103fa7 --- /dev/null +++ b/vsintegration/src/unittests/Tests.ProjectSystem.UpToDate.fs @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace UnitTests.Tests.ProjectSystem + +// System namespaces +open System +open System.Collections.Generic +open System.Globalization +open System.IO +open System.Text +open System.Text.RegularExpressions + +// VS namespaces +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.FSharp.ProjectSystem + +// Internal unittest namespaces +open NUnit.Framework +open Salsa +open UnitTests.TestLib.Utils.Asserts +open UnitTests.TestLib.Utils.FilesystemHelpers +open UnitTests.TestLib.ProjectSystem + +[] +type UpToDate() = + inherit TheTests() + + [] + member public this.ItemInputs () = + this.MakeProjectAndDo(["file1.fs"], [], @" + + + + + + + ", (fun project -> + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let config = project.ConfigProvider.GetProjectConfiguration(configNameDebug) + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + + let sourcePath = Path.Combine(project.ProjectFolder, "file1.fs") + let contentPath = Path.Combine(project.ProjectFolder, "content.txt") + let resourcePath = Path.Combine(project.ProjectFolder, "resource.txt") + let nonePath = Path.Combine(project.ProjectFolder, "none.txt") + let embedPath = Path.Combine(project.ProjectFolder, "embedresource.txt") + + let startTime = DateTime.Now + + File.AppendAllText(sourcePath, "printfn \"hello\"") + File.AppendAllText(contentPath, "some content") + File.AppendAllText(resourcePath, "some resource") + File.AppendAllText(nonePath, "none") + File.AppendAllText(embedPath, "some embedded resource") + + Assert.IsFalse(config.IsUpToDate(logger, true)) + project.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config.IsUpToDate(logger, true)) + + // None items should not affect up-to-date + File.SetLastWriteTime(nonePath, DateTime.Now.AddMinutes(5.)) + Assert.IsTrue(config.IsUpToDate(logger, true)) + + for path in [sourcePath; contentPath; resourcePath; embedPath] do + printfn "Testing path %s" path + + // touch file + File.SetLastWriteTime(path, DateTime.Now.AddMinutes(5.)) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + + // delete file + let originalContent = File.ReadAllText(path) + File.Delete(path) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.AppendAllText(path, originalContent) + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + )) + + [] + member public this.PropertyInputs () = + this.MakeProjectAndDo(["file1.fs"], [], @" + + ver.txt + key.txt + + ", (fun project -> + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let config = project.ConfigProvider.GetProjectConfiguration(configNameDebug) + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + + let sourcePath = Path.Combine(project.ProjectFolder, "file1.fs") + let verPath = Path.Combine(project.ProjectFolder, "ver.txt") + let keyPath = Path.Combine(project.ProjectFolder, "key.txt") + + let startTime = DateTime.Now + + File.AppendAllText(sourcePath, "printfn \"hello\"") + File.AppendAllText(verPath, "1.2.3.4") + File.AppendAllText(keyPath, "a key") + + project.SetConfiguration(config.ConfigCanonicalName); + Assert.IsFalse(config.IsUpToDate(logger, true)) + project.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config.IsUpToDate(logger, true)) + + for path in [verPath; keyPath] do + printfn "Testing path %s" path + + // touch file + File.SetLastWriteTime(path, DateTime.Now.AddMinutes(5.)) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + + // delete file + let originalContent = File.ReadAllText(path) + File.Delete(path) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.AppendAllText(path, originalContent) + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + )) + + [] + member public this.ProjectFile () = + this.MakeProjectAndDoWithProjectFile(["file1.fs"], [], "", (fun project projFileName -> + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let config = project.ConfigProvider.GetProjectConfiguration(configNameDebug) + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + let absFilePath = Path.Combine(project.ProjectFolder, "file1.fs") + let startTime = DateTime.Now + File.AppendAllText(absFilePath, "printfn \"hello\"") + + Assert.IsFalse(config.IsUpToDate(logger, true)) + project.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config.IsUpToDate(logger, true)) + + // touch proj file + File.SetLastWriteTime(projFileName, DateTime.Now.AddMinutes(5.)) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.SetLastWriteTime(projFileName, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + )) + + [] + member public this.References () = + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + + DoWithTempFile "Proj1.fsproj" (fun proj1Path -> + File.AppendAllText(proj1Path, TheTests.SimpleFsprojText( + ["File1.fs"], // + [], // + "v4.5")) // other stuff + use project1 = TheTests.CreateProject(proj1Path) + let sourcePath1 = Path.Combine(project1.ProjectFolder, "File1.fs") + File.AppendAllText(sourcePath1, "namespace Proj1\r\n") + File.AppendAllText(sourcePath1, "module Test =\r\n") + File.AppendAllText(sourcePath1, " let X = 5\r\n") + + let config1 = project1.ConfigProvider.GetProjectConfiguration(configNameDebug) + + Assert.IsFalse(config1.IsUpToDate(logger, true)) + project1.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config1.IsUpToDate(logger, true)) + + let output1 = Path.Combine(project1.ProjectFolder, "bin\\debug", project1.OutputFileName) + + DoWithTempFile "Proj2.fsproj" (fun proj2Path -> + File.AppendAllText(proj2Path, TheTests.SimpleFsprojText( + ["File2.fs"], // + [output1], // + "v4.5")) // other stuff + use project2 = TheTests.CreateProject(proj2Path) + let sourcePath2 = Path.Combine(project2.ProjectFolder, "File2.fs") + File.AppendAllText(sourcePath2, "open Proj1\r\n") + File.AppendAllText(sourcePath2, "let x = Test.X") + + let config2 = project2.ConfigProvider.GetProjectConfiguration(configNameDebug) + let startTime = DateTime.Now + + Assert.IsFalse(config2.IsUpToDate(logger, true)) + project2.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config2.IsUpToDate(logger, true)) + + // reference is updated + File.SetLastWriteTime(output1, DateTime.Now.AddMinutes(5.)) + Assert.IsFalse(config2.IsUpToDate(logger, true)) + File.SetLastWriteTime(output1, startTime) + Assert.IsTrue(config2.IsUpToDate(logger, true)) + + // reference is missing + File.Delete(output1) + Assert.IsFalse(config2.IsUpToDate(logger, true)) + ) + ) + + [] + member public this.OutputFiles () = + this.MakeProjectAndDo(["file1.fs"], [], @" + + bin\Debug\Test.XML + true + full + ", (fun project -> + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let config = project.ConfigProvider.GetProjectConfiguration(configNameDebug) + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + let sourcePath = Path.Combine(project.ProjectFolder, "file1.fs") + + let exeObjPath = Path.Combine(project.ProjectFolder, "obj\\x86\\debug", project.OutputFileName) + let exeBinpath = Path.Combine(project.ProjectFolder, "bin\\debug\\", project.OutputFileName) + let pdbObjPath = Regex.Replace(exeObjPath, "exe$", "pdb") + let pdbBinPath = Regex.Replace(exeBinpath, "exe$", "pdb") + let xmlDocPath = Regex.Replace(exeBinpath, "exe$", "xml") + + File.AppendAllText(sourcePath, "printfn \"hello\"") + + Assert.IsFalse(config.IsUpToDate(logger, true)) + project.Build(configNameDebug, output, "Build") |> ignore + Assert.IsTrue(config.IsUpToDate(logger, true)) + + let startTime = DateTime.Now + + for path in [exeObjPath; exeBinpath; pdbObjPath; pdbBinPath; xmlDocPath] do + printfn "Testing output %s" path + + // touch file + File.SetLastWriteTime(path, DateTime.Now.AddMinutes(-5.)) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + + // delete file + let originalContent = File.ReadAllBytes(path) + File.Delete(path) + Assert.IsFalse(config.IsUpToDate(logger, true)) + + File.WriteAllBytes(path, originalContent) + File.SetLastWriteTime(path, startTime) + Assert.IsTrue(config.IsUpToDate(logger, true)) + )) + + [] + member public this.ConfigChanges () = + this.MakeProjectAndDo(["file1.fs"], [], "", (fun project -> + let configNameDebugx86 = ConfigCanonicalName("Debug", "x86") + let configNameReleasex86 = ConfigCanonicalName("Release", "x86") + let configNameDebugAnyCPU = ConfigCanonicalName("Debug", "AnyCPU") + let configNameReleaseAnyCPU = ConfigCanonicalName("Release", "AnyCPU") + + let debugConfigx86 = project.ConfigProvider.GetProjectConfiguration(configNameDebugx86) + let releaseConfigx86 = project.ConfigProvider.GetProjectConfiguration(configNameReleasex86) + let debugConfigAnyCPU = project.ConfigProvider.GetProjectConfiguration(configNameDebugAnyCPU) + let releaseConfigAnyCPU = project.ConfigProvider.GetProjectConfiguration(configNameReleaseAnyCPU) + + let output = VsMocks.vsOutputWindowPane(ref []) + let logger = OutputWindowLogger.CreateUpToDateCheckLogger(output) + + let sourcePath = Path.Combine(project.ProjectFolder, "file1.fs") + File.AppendAllText(sourcePath, "printfn \"hello\"") + + Assert.IsFalse(debugConfigx86.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigx86.IsUpToDate(logger, true)) + Assert.IsFalse(debugConfigAnyCPU.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigAnyCPU.IsUpToDate(logger, true)) + + project.Build(configNameDebugx86, output, "Build") |> ignore + Assert.IsTrue(debugConfigx86.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigx86.IsUpToDate(logger, true)) + Assert.IsFalse(debugConfigAnyCPU.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigAnyCPU.IsUpToDate(logger, true)) + + project.Build(configNameReleasex86, output, "Build") |> ignore + Assert.IsTrue(debugConfigx86.IsUpToDate(logger, true)) + Assert.IsTrue(releaseConfigx86.IsUpToDate(logger, true)) + Assert.IsFalse(debugConfigAnyCPU.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigAnyCPU.IsUpToDate(logger, true)) + + project.Build(configNameDebugAnyCPU, output, "Build") |> ignore + Assert.IsTrue(debugConfigx86.IsUpToDate(logger, true)) + Assert.IsTrue(releaseConfigx86.IsUpToDate(logger, true)) + Assert.IsTrue(debugConfigAnyCPU.IsUpToDate(logger, true)) + Assert.IsFalse(releaseConfigAnyCPU.IsUpToDate(logger, true)) + + project.Build(configNameReleaseAnyCPU, output, "Build") |> ignore + Assert.IsTrue(debugConfigx86.IsUpToDate(logger, true)) + Assert.IsTrue(releaseConfigx86.IsUpToDate(logger, true)) + Assert.IsTrue(debugConfigAnyCPU.IsUpToDate(logger, true)) + Assert.IsTrue(releaseConfigAnyCPU.IsUpToDate(logger, true)) + )) + + [] + member public this.UTDCheckEnabled () = + this.MakeProjectAndDo(["file1.fs"], [], @" + + true + + ", (fun project -> + let configNameDebug = ConfigCanonicalName("Debug", "x86") + let config = project.ConfigProvider.GetProjectConfiguration(configNameDebug) + + Assert.IsFalse(config.IsFastUpToDateCheckEnabled()) + )) diff --git a/vsintegration/src/unittests/Unittests.fsproj b/vsintegration/src/unittests/Unittests.fsproj index 5055eb2b877..ee47cf1caee 100644 --- a/vsintegration/src/unittests/Unittests.fsproj +++ b/vsintegration/src/unittests/Unittests.fsproj @@ -62,6 +62,7 @@ + Unittests.dll.config diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/IDEBuildLogger.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/IDEBuildLogger.cs index da143b8895c..772a80e1a3d 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/IDEBuildLogger.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/IDEBuildLogger.cs @@ -19,26 +19,26 @@ namespace Microsoft.VisualStudio.FSharp.ProjectSystem { - - /// - /// This class implements an MSBuild logger that output events to VS outputwindow and tasklist. - /// - [ComVisible(true)] - public sealed class IDEBuildLogger : Logger - { - #region fields - // TODO: Remove these constants when we have a version that suppoerts getting the verbosity using automation. -#if FX_ATLEAST_45 - private string buildVerbosityRegistryRoot = @"Software\Microsoft\VisualStudio\12.0"; -#else - private string buildVerbosityRegistryRoot = @"Software\Microsoft\VisualStudio\10.0"; -#endif - private const string buildVerbosityRegistrySubKey = @"General"; - private const string buildVerbosityRegistryKey = "MSBuildLoggerVerbosity"; - // TODO: Re-enable this constants when we have a version that suppoerts getting the verbosity using automation. - //private const string EnvironmentCategory = "Environment"; - //private const string ProjectsAndSolutionSubCategory = "ProjectsAndSolution"; - //private const string BuildAndRunPage = "BuildAndRun"; + public static class LoggingConstants + { + public const string DefaultVSRegistryRoot = @"Software\Microsoft\VisualStudio\12.0"; + public const string BuildVerbosityRegistrySubKey = @"General"; + public const string BuildVerbosityRegistryValue = "MSBuildLoggerVerbosity"; + public const string UpToDateVerbosityRegistryValue = "U2DCheckVerbosity"; + } + /// + /// This class implements an MSBuild logger that output events to VS outputwindow and tasklist. + /// + [ComVisible(true)] + public sealed class IDEBuildLogger : Logger + { + #region fields + // TODO: Remove these constants when we have a version that supports getting the verbosity using automation. + private string buildVerbosityRegistryRoot = LoggingConstants.DefaultVSRegistryRoot; + // TODO: Re-enable this constants when we have a version that suppoerts getting the verbosity using automation. + //private const string EnvironmentCategory = "Environment"; + //private const string ProjectsAndSolutionSubCategory = "ProjectsAndSolution"; + //private const string BuildAndRunPage = "BuildAndRun"; private int currentIndent; private IVsOutputWindowPane outputWindowPane; @@ -786,17 +786,17 @@ private string FormatMessage(string message) /// /// Sets the verbosity level. /// - private void SetVerbosity() - { - // TODO: This should be replaced when we have a version that supports automation. + private void SetVerbosity() + { + // TODO: This should be replaced when we have a version that supports automation. if (!this.haveCachedRegistry) { - string verbosityKey = String.Format(CultureInfo.InvariantCulture, @"{0}\{1}", BuildVerbosityRegistryRoot, buildVerbosityRegistrySubKey); + string verbosityKey = String.Format(CultureInfo.InvariantCulture, @"{0}\{1}", BuildVerbosityRegistryRoot, LoggingConstants.BuildVerbosityRegistrySubKey); using (RegistryKey subKey = Registry.CurrentUser.OpenSubKey(verbosityKey)) { if (subKey != null) { - object valueAsObject = subKey.GetValue(buildVerbosityRegistryKey); + object valueAsObject = subKey.GetValue(LoggingConstants.BuildVerbosityRegistryValue); if (valueAsObject != null) { this.Verbosity = (LoggerVerbosity)((int)valueAsObject); @@ -806,11 +806,80 @@ private void SetVerbosity() this.haveCachedRegistry = true; } - // TODO: Continue this code to get the Verbosity when we have a version that supports automation to get the Verbosity. - //EnvDTE.DTE dte = this.serviceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE; - //EnvDTE.Properties properties = dte.get_Properties(EnvironmentCategory, ProjectsAndSolutionSubCategory); - } + // TODO: Continue this code to get the Verbosity when we have a version that supports automation to get the Verbosity. + //EnvDTE.DTE dte = this.serviceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE; + //EnvDTE.Properties properties = dte.get_Properties(EnvironmentCategory, ProjectsAndSolutionSubCategory); + } #endregion - } + } + + /// + /// Helper for logging to the output window + /// + public sealed class OutputWindowLogger + { + private readonly Func predicate; + private readonly Action print; + + /// + /// Helper to create output window logger for project up-to-date check + /// + /// Output window pane to use for logging + /// Logger + public static OutputWindowLogger CreateUpToDateCheckLogger(IVsOutputWindowPane pane) + { + string upToDateVerbosityKey = + String.Format(CultureInfo.InvariantCulture, @"{0}\{1}", LoggingConstants.DefaultVSRegistryRoot, LoggingConstants.BuildVerbosityRegistrySubKey); + var shouldLog = false; + using (RegistryKey subKey = Registry.CurrentUser.OpenSubKey(upToDateVerbosityKey)) + { + if (subKey != null) + { + object valueAsObject = subKey.GetValue(LoggingConstants.UpToDateVerbosityRegistryValue); + if (valueAsObject != null && valueAsObject is int) + { + shouldLog = ((int)valueAsObject) == 1; + } + } + } + + return new OutputWindowLogger(() => shouldLog, pane); + } + + /// + /// Creates a logger instance + /// + /// Predicate that will be called when logging. Should return true if logging is to be performed, false otherwise. + /// The output pane where logging should be targeted + public OutputWindowLogger(Func shouldLog, IVsOutputWindowPane pane) + { + this.predicate = shouldLog; + + if (pane is IVsOutputWindowPaneNoPump) + { + var asNoPump = pane as IVsOutputWindowPaneNoPump; + this.print = (s) => asNoPump.OutputStringNoPump(s); + } + else + { + this.print = (s) => pane.OutputStringThreadSafe(s); + } + } + + /// + /// Logs a message to the output window, if the original predicate returns true + /// + /// Log message, can be a String.Format-style format string + /// Optional aruments for format string + public void WriteLine(string message, params object[] args) + { + if (this.predicate()) + { + var s = String.Format(message, args); + s = String.Format("{0}{1}", s, Environment.NewLine); + this.print(s); + } + } + } } diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Output.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Output.cs index 29c5768b79c..81175b5454b 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Output.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Output.cs @@ -18,6 +18,18 @@ internal class Output : IVsOutput2 private Microsoft.Build.Execution.ProjectItemInstance output; private ProjectNode project; + /// + /// Easy access to canonical name + /// + internal string CanonicalName + { + get + { + string canonicalName; + return ErrorHandler.Succeeded(get_CanonicalName(out canonicalName)) ? canonicalName : null; + } + } + /// /// Constructor for IVSOutput2 implementation /// @@ -34,6 +46,16 @@ public Output(ProjectNode projectManager, Microsoft.Build.Execution.ProjectItemI project = projectManager; output = outputAssembly; } + + /// + /// Easy access to output properties + /// + internal string GetMetadata(string name) + { + object value; + return ErrorHandler.Succeeded(get_Property(name, out value)) ? value as string : null; + } + #region IVsOutput2 Members public int get_CanonicalName(out string pbstrCanonicalName) diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/OutputGroup.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/OutputGroup.cs index b5d1d7559db..e23b316369a 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/OutputGroup.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/OutputGroup.cs @@ -58,6 +58,32 @@ public class OutputGroup : IVsOutputGroup2 { get { return targetName; } } + + /// + /// Easy access to the canonical name of the group. + /// + internal string CanonicalName + { + get + { + string canonicalName; + ErrorHandler.ThrowOnFailure(get_CanonicalName(out canonicalName)); + return canonicalName; + } + } + + /// + /// Easy access to outputs + /// + internal Output[] Outputs + { + get + { + Refresh(); + return outputs.ToArray(); + } + } + #endregion #region ctors diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectConfig.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectConfig.cs index 2b123d35eee..00607561e17 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectConfig.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectConfig.cs @@ -1429,6 +1429,181 @@ int IVsProjectFlavorCfg.get_CfgType(ref Guid iidCfg, out IntPtr ppCfg) public string Platform { get { return this.configCanonicalName.Platform; } } + + private static bool IsPossibleOutputGroup(string groupName) + { + return groupName != "SourceFiles"; + } + + private static DateTime? TryGetLastWriteTimeUtc(string path, OutputWindowLogger logger) + { + Exception exn = null; + + try + { + if (File.Exists(path)) + return File.GetLastWriteTimeUtc(path); + } + catch (Exception ex) + { + exn = ex; + } + + if (exn != null) + { + logger.WriteLine("Failed to access {0}: {1}", path, exn.Message); + logger.WriteLine(exn.ToString()); + } + + return null; + } + + internal bool GetUTDCheckInputs(ref HashSet inputs) + { + // the project file itself + inputs.Add(Utilities.CanonicalizeFileNameNoThrow(this.project.BuildProject.FullPath)); + + var projDir = this.project.BuildProject.DirectoryPath; + + // well-known types of input items + var itemTypes = new string[] { "Compile", "Content", "Resource", "EmbeddedResource" }; + foreach (var itemType in itemTypes) + { + foreach (var item in this.project.BuildProject.GetItems(itemType)) + { + inputs.Add(Utilities.CanonicalizeFileNameNoThrow(Path.Combine(projDir, item.EvaluatedInclude))); + } + } + + // other well-known inputs exposed as properties + var properties = new string[] { "Win32Manifest", "Win32Resource", "AssemblyOriginatorKeyFile", "KeyOriginatorFile", "ApplicationIcon", "VersionFile" }; + foreach (var prop in properties) + { + var propVal = this.project.BuildProject.GetPropertyValue(prop); + if (!String.IsNullOrWhiteSpace(propVal)) + inputs.Add(Utilities.CanonicalizeFileNameNoThrow(Path.Combine(projDir, propVal))); + } + + // assembly and project references + foreach (var reference in this.project .GetReferenceContainer().EnumReferences()) + { + if (reference is AssemblyReferenceNode) + inputs.Add(Utilities.CanonicalizeFileNameNoThrow(reference.Url)); + else if (reference is ProjectReferenceNode) + inputs.Add(Utilities.CanonicalizeFileNameNoThrow((reference as ProjectReferenceNode).ReferencedProjectOutputPath)); + else + // some reference type we don't know about + return false; + } + + return true; + } + + internal bool GetUTDCheckOutputs(ref HashSet outputs, HashSet inputs) + { + // Output groups give us the paths to the following outputs + // result EXE or DLL in "obj" dir + // PDB file in "obj" dir (if project is configured to create this) + // XML doc file in "bin" dir (if project is configured to create this) + foreach (var output in OutputGroups + .Where(g => IsPossibleOutputGroup(g.CanonicalName)) + .SelectMany(x => x.Outputs) + .Select(o => Utilities.CanonicalizeFileNameNoThrow(o.CanonicalName)) + .Where((path) => !inputs.Contains(path))) // some "outputs" are really inputs (e.g. app.config files) + { + outputs.Add(output); + } + + // final binplace of built assembly + var outputAssembly = this.project.GetOutputAssembly(this.ConfigCanonicalName); + outputs.Add(Utilities.CanonicalizeFileNameNoThrow(outputAssembly)); + + // final PDB path + if (this.DebugSymbols && + (outputAssembly.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || outputAssembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))) + { + var pdbPath = outputAssembly.Remove(outputAssembly.Length - 4) + ".pdb"; + outputs.Add(Utilities.CanonicalizeFileNameNoThrow(pdbPath)); + } + + return true; + } + + // there is a well-known property users can specify that signals for UTD check to be disabled + internal bool IsFastUpToDateCheckEnabled() + { + var fastUTDPropVal = this.project.BuildProject.GetPropertyValue("DisableFastUpToDateCheck"); + if (String.IsNullOrWhiteSpace(fastUTDPropVal)) + return true; + if (String.Equals(fastUTDPropVal, "true", StringComparison.OrdinalIgnoreCase)) + return false; + return true; + } + + internal bool IsUpToDate(OutputWindowLogger logger, bool testing) + { + logger.WriteLine("Checking whether {0} needs to be rebuilt:", ProjectMgr.Caption); + + // in batch build it is possible that config is out of sync. + // in this case, don't assume we are up to date + ConfigCanonicalName activeConfig = default(ConfigCanonicalName); + if(!Utilities.TryGetActiveConfigurationAndPlatform(ServiceProvider.GlobalProvider, this.project, out activeConfig) || + activeConfig != this.ConfigCanonicalName) + { + logger.WriteLine("Not up to date: active confic does not match project config. Active: {0} Project: {1}", activeConfig, this.ConfigCanonicalName); + if (!testing) + return false; + } + + var inputs = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!GetUTDCheckInputs(ref inputs)) + return false; + + var outputs = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!GetUTDCheckOutputs(ref outputs, inputs)) + return false; + + // determine the oldest output timestamp + DateTime stalestOutputTime = DateTime.MaxValue.ToUniversalTime(); + foreach (var output in outputs) + { + var timeStamp = TryGetLastWriteTimeUtc(output, logger); + if (!timeStamp.HasValue) + { + logger.WriteLine("Declaring project NOT up to date, can't find expected output {0}", output); + return false; + } + + logger.WriteLine(" Output: {0} {1}", timeStamp.Value.ToLocalTime(), output); + + if (stalestOutputTime > timeStamp.Value) + stalestOutputTime = timeStamp.Value; + } + + // determine the newest input timestamp + DateTime freshestInputTime = DateTime.MinValue.ToUniversalTime(); + foreach (var input in inputs) + { + var timeStamp = TryGetLastWriteTimeUtc(input, logger); + if (!timeStamp.HasValue) + { + logger.WriteLine("Declaring project NOT up to date, can't find expected input {0}", input); + return false; + } + + logger.WriteLine(" Input: {0} {1}", timeStamp.Value.ToLocalTime(), input); + + if (freshestInputTime < timeStamp.Value) + freshestInputTime = timeStamp.Value; + } + + logger.WriteLine("Freshest input: {0}", freshestInputTime.ToLocalTime()); + logger.WriteLine("Stalest output: {0}", stalestOutputTime.ToLocalTime()); + logger.WriteLine("Up to date: {0}", freshestInputTime <= stalestOutputTime); + + // if all outputs are younger than all inuts, we are up to date + return freshestInputTime <= stalestOutputTime; + } } internal class ClassLibraryCannotBeStartedDirectlyException : COMException @@ -1577,10 +1752,13 @@ public virtual int QueryStartUpToDateCheck(uint options, int[] supported, int[] { CCITracing.TraceCall(); config.PrepareBuild(false); + + int utdSupported = config.IsFastUpToDateCheckEnabled() ? 1 : 0; + if (supported != null && supported.Length > 0) - supported[0] = 0; // TODO: + supported[0] = utdSupported; if (ready != null && ready.Length > 0) - ready[0] = (IsInProgress()) ? 0 : 1; + ready[0] = utdSupported; return VSConstants.S_OK; } @@ -1619,8 +1797,8 @@ public virtual int StartClean(IVsOutputWindowPane pane, uint options) public virtual int StartUpToDateCheck(IVsOutputWindowPane pane, uint options) { CCITracing.TraceCall(); - - return VSConstants.E_NOTIMPL; + + return config.IsUpToDate(OutputWindowLogger.CreateUpToDateCheckLogger(pane), false) ? VSConstants.S_OK : VSConstants.E_FAIL; } public virtual int Stop(int fsync) diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectNode.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectNode.cs index 0fa98d9fd3c..3dfe6e6bac3 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectNode.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectNode.cs @@ -770,6 +770,14 @@ public virtual Guid ProjectIDGuid /// public bool CanUseTargetFSharpCoreReference { get; set; } + /// + /// Easy access to the collection of visible, user-defined project items + /// + public IEnumerable VisibleItems + { + get { return MSBuildProject.GetStaticAndVisibleItemsInOrder(this.buildProject); } + } + #region overridden properties public override int MenuCommandId { diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectSystem.Base.csproj b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectSystem.Base.csproj index 700a30929e4..e0c78da6060 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectSystem.Base.csproj +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/ProjectSystem.Base.csproj @@ -76,6 +76,7 @@ + diff --git a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Utilities.cs b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Utilities.cs index 18366033fd9..61f21903b12 100644 --- a/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Utilities.cs +++ b/vsintegration/src/vs/FsPkgs/FSharp.Project/Common.Source.CSharp/Project/Utilities.cs @@ -902,6 +902,21 @@ public static void RecursivelyCopyDirectory(string source, string target) return fullPath; } + /// + /// Attempts a call to CanonicalizeFileName, but returns the input unchanged if that method throws + /// + /// File name to canonicalize + /// Canonicalized file name if possible, otherwise returns input unchanged + public static string CanonicalizeFileNameNoThrow(string anyFileName) + { + try + { + return CanonicalizeFileName(anyFileName); + } + catch { } + + return anyFileName; + } /// /// Determines if a file is a template.