diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4b4120db5..3e5ccb9d2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: - package-ecosystem: "nuget" # location of package manifests - directory: "/src/Notepads" + directory: "/src/Notepads.UnitTest" schedule: interval: "daily" commit-message: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 040676c69..4e21747fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: Notepads CI/CD Pipeline -on: +on: push: paths-ignore: - '**.md' @@ -106,6 +106,8 @@ jobs: is_release: ${{ matrix.release }} env: SOLUTION_NAME: src\Notepads.sln + PROJECT_NAME: src\Notepads\Notepads.csproj + TEST_PROJECT_NAME: src\Notepads.UnitTest\Notepads.UnitTest.csproj CONFIGURATION: ${{ matrix.configuration }} DEFAULT_DIR: ${{ github.workspace }} steps: @@ -124,6 +126,10 @@ jobs: id: setup-nuget uses: NuGet/setup-nuget@v1.0.5 + - name: Setup VSTest + id: setup-vstest + uses: darenm/Setup-VSTest@v1 + - name: Checkout repository id: checkout_repo uses: actions/checkout@v2 @@ -156,7 +162,7 @@ jobs: name: Bump GitHub tag id: tag_generator uses: soumyamahunt/github-tag-action@test-other-ver-support - with: + with: github_token: ${{ secrets.GITHUB_TOKEN }} latest_ver: ${{ steps.check_latest_tag.outputs.semver }} default_bump: true @@ -212,7 +218,7 @@ jobs: id: init_sonar_scanner shell: pwsh run: | - $LOWERCASE_REPOSITORY_NAME = "${{ github.event.repository.name }}".ToLower() + $LOWERCASE_REPOSITORY_NAME = "${{ github.event.repository.name }}".ToLower() .\.sonar\scanner\dotnet-sonarscanner begin ` /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" ` /o:"$LOWERCASE_REPOSITORY_NAME" ` @@ -250,9 +256,10 @@ jobs: id: build_app shell: pwsh run: | - msbuild $env:SOLUTION_NAME ` + msbuild $env:PROJECT_NAME ` /p:Platform=$env:PLATFORM ` /p:Configuration=$env:CONFIGURATION ` + /p:AllowUnsafeBlocks=true ` /p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE ` /p:AppxBundle=$env:APPX_BUNDLE ` /p:AppxPackageSigningEnabled=$env:APPX_PACKAGE_SIGNING_ENABLED ` @@ -277,9 +284,10 @@ jobs: id: build_app_arm_debug shell: pwsh run: | - msbuild $env:SOLUTION_NAME ` + msbuild $env:PROJECT_NAME ` /p:Platform=$env:PLATFORM ` /p:Configuration=$env:CONFIGURATION ` + /p:AllowUnsafeBlocks=true ` /p:UapAppxPackageBuildMode=$env:UAP_APPX_PACKAGE_BUILD_MODE ` /p:AppxBundle=$env:APPX_BUNDLE ` /p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS @@ -314,6 +322,24 @@ jobs: name: Build artifacts path: Artifacts/ + - if: matrix.debug || matrix.release + name: Run Unit Tests + id: run_unittest + shell: pwsh + run: | + msbuild $env:TEST_PROJECT_NAME ` + /p:Platform=$env:PLATFORM ` + /p:Configuration=$env:CONFIGURATION ` + /p:AllowUnsafeBlocks=true ` + /p:AppxBundlePlatforms=$env:APPX_BUNDLE_PLATFORMS + + $TEST_PATH = "$([System.IO.Path]::GetDirectoryName($env:TEST_PROJECT_NAME))$( + )\bin\$($env:PLATFORM)\$($env:CONFIGURATION)\Notepads.UnitTest.build.appxrecipe" + vstest.console.exe $TEST_PATH /framework:FrameworkUap10 + env: + PLATFORM: x64 + APPX_BUNDLE_PLATFORMS: x64 + cd: # "This job will execute when the workflow is triggered on a 'push event', the target branch is 'master' and the commit is intended to be a release." if: needs.ci.outputs.is_release == 'true' && needs.ci.outputs.new_version != '' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2d0ced9ff..ae1dcfaed 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,7 +15,8 @@ pool: vmImage: 'windows-latest' variables: - solution: '**/*.sln' + solution: '**\*.sln' + project: 'src\Notepads\Notepads.csproj' buildPlatform: 'x86|x64|arm64' buildConfiguration: 'Production' appxPackageDir: '$(build.artifactStagingDirectory)\AppxPackages\\' @@ -30,10 +31,11 @@ steps: - task: VSBuild@1 inputs: platform: 'x64' - solution: '$(solution)' + solution: '$(project)' configuration: '$(buildConfiguration)' - msbuildArgs: '/p:AppxBundlePlatforms="$(buildPlatform)" + msbuildArgs: '/p:AllowUnsafeBlocks=true + /p:AppxBundlePlatforms="$(buildPlatform)" /p:AppxPackageDir="$(appxPackageDir)" /p:AppxBundle=Always /p:UapAppxPackageBuildMode=StoreUpload - /p:AppxPackageSigningEnabled=false' + /p:AppxPackageSigningEnabled=false' \ No newline at end of file diff --git a/src/Notepads.UnitTest/CommandLineTest.cs b/src/Notepads.UnitTest/CommandLineTest.cs new file mode 100644 index 000000000..d31eb9352 --- /dev/null +++ b/src/Notepads.UnitTest/CommandLineTest.cs @@ -0,0 +1,118 @@ +namespace Notepads.UnitTest +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Notepads.Utilities; + + [TestClass] + public class CommandLineTest + { + private const string cmdArg = "notepads"; + private const string pwshArg = "\"C:\\Users\\Test User\\AppData\\Local\\Microsoft\\WindowsApps\\Notepads.exe\""; + + private const string windowsRoot = "C:"; + private const string wslRoot = @"\\wsl$\Ubuntu"; + + private static IEnumerable GetTestData(string root, string alias) + { + yield return new string[] { $"{root}\\", $"{alias} \"{root}\\1.txt\"", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"{root}\\1 2.txt\"", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} {root}\\1.txt", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} {root}\\1 2.txt", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"{root}\\2.txt\"", $"{root}\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"{root}\\2 3.txt\"", $"{root}\\2 3.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} {root}\\2.txt", $"{root}\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} {root}\\2 3.txt", $"{root}\\2 3.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"\\1.txt\"", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"\\1 2.txt\"", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \\1.txt", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \\1 2.txt", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"\\2.txt\"", $"{root}\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"\\2 3.txt\"", $"{root}\\2 3.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \\2.txt", $"{root}\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \\2 3.txt", $"{root}\\2 3.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"1.txt\"", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"1 2.txt\"", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} 1.txt", $"{root}\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} 1 2.txt", $"{root}\\1 2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"2.txt\"", $"{root}\\1\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"2 3.txt\"", $"{root}\\1\\2 3.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} 2.txt", $"{root}\\1\\2.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} 2 3.txt", $"{root}\\1\\2 3.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"1\\2.txt\"", $"{root}\\1\\2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"1 2\\3 4.txt\"", $"{root}\\1 2\\3 4.txt" }; + yield return new string[] { $"{root}\\", $"{alias} 1\\2.txt", $"{root}\\1\\2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} 1 2\\3 4.txt", $"{root}\\1 2\\3 4.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"2\\3.txt\"", $"{root}\\1\\2\\3.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} \"2 3\\4 5.txt\"", $"{root}\\1\\2 3\\4 5.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} 2\\3.txt", $"{root}\\1\\2\\3.txt" }; + yield return new string[] { $"{root}\\1", $"{alias} 2 3\\4 5.txt", $"{root}\\1\\2 3\\4 5.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"D:\\1.txt\"", $"D:\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"D:\\1\\2.txt\"", $"D:\\1\\2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} D:\\1.txt", $"D:\\1.txt" }; + yield return new string[] { $"{root}\\", $"{alias} D:\\1\\2.txt", $"D:\\1\\2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"D:\\1 2.txt\"", $"D:\\1 2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} \"D:\\1 2\\3 4.txt\"", $"D:\\1 2\\3 4.txt" }; + yield return new string[] { $"{root}\\", $"{alias} D:\\1 2.txt", $"D:\\1 2.txt" }; + yield return new string[] { $"{root}\\", $"{alias} D:\\1 2\\3 4.txt", $"D:\\1 2\\3 4.txt" }; + } + + private static IEnumerable GetCommandPromptTestData() + { + return GetTestData(windowsRoot, cmdArg); + } + + private static IEnumerable GetPowerShellTestData() + { + return GetTestData(windowsRoot, pwshArg); + } + + private static IEnumerable GetWslTestData() + { + foreach(var data in GetTestData(wslRoot, pwshArg)) + { + yield return data; + } + + yield return new string[] { $"{wslRoot}\\", $"{pwshArg} \"/etc/sudo.conf\"", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\", $"{pwshArg} \"etc/sudo.conf\"", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\", $"{pwshArg} /etc/sudo.conf", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\", $"{pwshArg} etc/sudo.conf", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\etc", $"{pwshArg} \"/etc/sudo.conf\"", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\etc", $"{pwshArg} \"sudo.conf\"", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\etc", $"{pwshArg} /etc/sudo.conf", $"{wslRoot}\\etc\\sudo.conf" }; + yield return new string[] { $"{wslRoot}\\etc", $"{pwshArg} sudo.conf", $"{wslRoot}\\etc\\sudo.conf" }; + } + + [DataTestMethod] + [DynamicData(nameof(GetCommandPromptTestData), DynamicDataSourceType.Method)] + public void CommandLineTestForCommandPrompt(string dir, string arg, string result) + { + Assert.AreEqual( + CommandLineUtility.GetAbsolutePathFromCommandLine(dir, arg), + result + ); + } + + [DataTestMethod] + [DynamicData(nameof(GetPowerShellTestData), DynamicDataSourceType.Method)] + public void CommandLineTestForPowerShell(string dir, string arg, string result) + { + Assert.AreEqual( + CommandLineUtility.GetAbsolutePathFromCommandLine(dir, arg), + result + ); + } + + [DataTestMethod] + [DynamicData(nameof(GetWslTestData), DynamicDataSourceType.Method)] + public void CommandLineTestForWsl(string dir, string arg, string result) + { + Assert.AreEqual( + CommandLineUtility.GetAbsolutePathFromCommandLine(dir, arg), + result + ); + } + } +} \ No newline at end of file diff --git a/src/Notepads.UnitTest/Notepads.UnitTest.csproj b/src/Notepads.UnitTest/Notepads.UnitTest.csproj new file mode 100644 index 000000000..444285579 --- /dev/null +++ b/src/Notepads.UnitTest/Notepads.UnitTest.csproj @@ -0,0 +1,222 @@ + + + + + Debug + x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354} + AppContainerExe + Properties + Notepads.UnitTest + Notepads.UnitTest + en-US + UAP + 10.0.19041.0 + 10.0.17763.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + $(VisualStudioVersion) + false + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x86 + bin\x86\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM + bin\ARM\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM64 + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM64 + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM64 + bin\ARM64\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x64 + bin\x64\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + PackageReference + + + + + + + + UnitTestApp.xaml + + + + + + MSBuild:Compile + Designer + + + + + Designer + + + + + 6.2.11 + + + 2.0.0 + + + 2.0.0 + + + + + {99274932-9e86-480c-8142-38525f80007d} + Notepads + + + + 14.0 + + + + \ No newline at end of file diff --git a/src/Notepads.UnitTest/Package.appxmanifest b/src/Notepads.UnitTest/Package.appxmanifest new file mode 100644 index 000000000..7bfcb162a --- /dev/null +++ b/src/Notepads.UnitTest/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + Notepads UnitTest + Jackie Liu + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Notepads.UnitTest/Properties/AssemblyInfo.cs b/src/Notepads.UnitTest/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..cf568f2e9 --- /dev/null +++ b/src/Notepads.UnitTest/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Notepads.UnitTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Notepads.UnitTest")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyMetadata("TargetPlatform","UAP")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/src/Notepads.UnitTest/UnitTestApp.xaml b/src/Notepads.UnitTest/UnitTestApp.xaml new file mode 100644 index 000000000..3a971d7ee --- /dev/null +++ b/src/Notepads.UnitTest/UnitTestApp.xaml @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/Notepads.UnitTest/UnitTestApp.xaml.cs b/src/Notepads.UnitTest/UnitTestApp.xaml.cs new file mode 100644 index 000000000..fcd8ac1f7 --- /dev/null +++ b/src/Notepads.UnitTest/UnitTestApp.xaml.cs @@ -0,0 +1,91 @@ +namespace Notepads.UnitTest +{ + using System; + using Windows.ApplicationModel; + using Windows.ApplicationModel.Activation; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + using Windows.UI.Xaml.Navigation; + + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + sealed partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + this.Suspending += OnSuspending; + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(LaunchActivatedEventArgs e) + { +#if DEBUG + if (System.Diagnostics.Debugger.IsAttached) + { + this.DebugSettings.EnableFrameRateCounter = true; + } +#endif + + Frame rootFrame = Window.Current.Content as Frame; + + // Do not repeat app initialization when the Window already has content, + // just ensure that the window is active + if (rootFrame == null) + { + // Create a Frame to act as the navigation context and navigate to the first page + rootFrame = new Frame(); + + rootFrame.NavigationFailed += OnNavigationFailed; + + if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) + { + //TODO: Load state from previously suspended application + } + + // Place the frame in the current Window + Window.Current.Content = rootFrame; + } + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI(); + + // Ensure the current window is active + Window.Current.Activate(); + + Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(e.Arguments); + } + + /// + /// Invoked when Navigation to a certain page fails + /// + /// The Frame which failed navigation + /// Details about the navigation failure + void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + { + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + } + + /// + /// Invoked when application execution is being suspended. Application state is saved + /// without knowing whether the application will be terminated or resumed with the contents + /// of memory still intact. + /// + /// The source of the suspend request. + /// Details about the suspend request. + private void OnSuspending(object sender, SuspendingEventArgs e) + { + var deferral = e.SuspendingOperation.GetDeferral(); + //TODO: Save application state and stop any background activity + deferral.Complete(); + } + } +} \ No newline at end of file diff --git a/src/Notepads.sln b/src/Notepads.sln index fad9f03e9..37a477f4f 100644 --- a/src/Notepads.sln +++ b/src/Notepads.sln @@ -15,19 +15,27 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notepads.UnitTest", "Notepads.UnitTest\Notepads.UnitTest.csproj", "{49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - Release|x86 = Release|x86 + Production|ARM = Production|ARM Production|ARM64 = Production|ARM64 Production|x64 = Production|x64 Production|x86 = Production|x86 + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM.ActiveCfg = Debug|ARM + {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM.Build.0 = Debug|ARM + {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM.Deploy.0 = Debug|ARM {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.Build.0 = Debug|ARM64 {99274932-9E86-480C-8142-38525F80007D}.Debug|ARM64.Deploy.0 = Debug|ARM64 @@ -37,15 +45,9 @@ Global {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.ActiveCfg = Debug|x86 {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.Build.0 = Debug|x86 {99274932-9E86-480C-8142-38525F80007D}.Debug|x86.Deploy.0 = Debug|x86 - {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.ActiveCfg = Release|ARM64 - {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Build.0 = Release|ARM64 - {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Deploy.0 = Release|ARM64 - {99274932-9E86-480C-8142-38525F80007D}.Release|x64.ActiveCfg = Release|x64 - {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Build.0 = Release|x64 - {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Deploy.0 = Release|x64 - {99274932-9E86-480C-8142-38525F80007D}.Release|x86.ActiveCfg = Release|x86 - {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Build.0 = Release|x86 - {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Deploy.0 = Release|x86 + {99274932-9E86-480C-8142-38525F80007D}.Production|ARM.ActiveCfg = Production|ARM + {99274932-9E86-480C-8142-38525F80007D}.Production|ARM.Build.0 = Production|ARM + {99274932-9E86-480C-8142-38525F80007D}.Production|ARM.Deploy.0 = Production|ARM {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.ActiveCfg = Production|ARM64 {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.Build.0 = Production|ARM64 {99274932-9E86-480C-8142-38525F80007D}.Production|ARM64.Deploy.0 = Production|ARM64 @@ -55,24 +57,78 @@ Global {99274932-9E86-480C-8142-38525F80007D}.Production|x86.ActiveCfg = Production|x86 {99274932-9E86-480C-8142-38525F80007D}.Production|x86.Build.0 = Production|x86 {99274932-9E86-480C-8142-38525F80007D}.Production|x86.Deploy.0 = Production|x86 + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM.ActiveCfg = Release|ARM + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM.Build.0 = Release|ARM + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM.Deploy.0 = Release|ARM + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.ActiveCfg = Release|ARM64 + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Build.0 = Release|ARM64 + {99274932-9E86-480C-8142-38525F80007D}.Release|ARM64.Deploy.0 = Release|ARM64 + {99274932-9E86-480C-8142-38525F80007D}.Release|x64.ActiveCfg = Release|x64 + {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Build.0 = Release|x64 + {99274932-9E86-480C-8142-38525F80007D}.Release|x64.Deploy.0 = Release|x64 + {99274932-9E86-480C-8142-38525F80007D}.Release|x86.ActiveCfg = Release|x86 + {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Build.0 = Release|x86 + {99274932-9E86-480C-8142-38525F80007D}.Release|x86.Deploy.0 = Release|x86 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM.ActiveCfg = Debug|ARM + {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM.Build.0 = Debug|ARM {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM64.ActiveCfg = Debug|ARM64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|ARM64.Build.0 = Debug|ARM64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x64.ActiveCfg = Debug|x64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x64.Build.0 = Debug|x64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x86.ActiveCfg = Debug|x86 {7AA5E631-B663-420E-A08F-002CD81DF855}.Debug|x86.Build.0 = Debug|x86 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.ActiveCfg = Release|ARM64 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.Build.0 = Release|ARM64 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.ActiveCfg = Release|x64 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.Build.0 = Release|x64 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.ActiveCfg = Release|x86 - {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.Build.0 = Release|x86 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM.ActiveCfg = Production|ARM + {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM.Build.0 = Production|ARM {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM64.ActiveCfg = Production|ARM64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|ARM64.Build.0 = Production|ARM64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x64.ActiveCfg = Production|x64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x64.Build.0 = Production|x64 {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x86.ActiveCfg = Production|x86 {7AA5E631-B663-420E-A08F-002CD81DF855}.Production|x86.Build.0 = Production|x86 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM.ActiveCfg = Release|ARM + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM.Build.0 = Release|ARM + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.ActiveCfg = Release|ARM64 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|ARM64.Build.0 = Release|ARM64 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.ActiveCfg = Release|x64 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x64.Build.0 = Release|x64 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.ActiveCfg = Release|x86 + {7AA5E631-B663-420E-A08F-002CD81DF855}.Release|x86.Build.0 = Release|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM.ActiveCfg = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM.Build.0 = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM.Deploy.0 = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM64.Build.0 = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x64.ActiveCfg = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x64.Build.0 = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x64.Deploy.0 = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x86.ActiveCfg = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x86.Build.0 = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Debug|x86.Deploy.0 = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM.ActiveCfg = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM.Build.0 = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM.Deploy.0 = Debug|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM64.ActiveCfg = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM64.Build.0 = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|ARM64.Deploy.0 = Debug|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x64.ActiveCfg = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x64.Build.0 = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x64.Deploy.0 = Debug|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x86.ActiveCfg = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x86.Build.0 = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Production|x86.Deploy.0 = Debug|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM.ActiveCfg = Release|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM.Build.0 = Release|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM.Deploy.0 = Release|ARM + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM64.ActiveCfg = Release|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM64.Build.0 = Release|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|ARM64.Deploy.0 = Release|ARM64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x64.ActiveCfg = Release|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x64.Build.0 = Release|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x64.Deploy.0 = Release|x64 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x86.ActiveCfg = Release|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x86.Build.0 = Release|x86 + {49BF8FC5-BBBF-400C-A4F2-7AA73B8F2354}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Notepads/Controls/TextEditor/ITextEditor.cs b/src/Notepads/Controls/TextEditor/ITextEditor.cs index 4b8feb04f..c02550c92 100644 --- a/src/Notepads/Controls/TextEditor/ITextEditor.cs +++ b/src/Notepads/Controls/TextEditor/ITextEditor.cs @@ -46,7 +46,7 @@ public interface ITextEditor string EditingFilePath { get; } - StorageFile EditingFile { get; } + IStorageFile EditingFile { get; } bool IsModified { get; } @@ -59,7 +59,7 @@ public interface ITextEditor bool DisplayLineHighlighter { get; set; } void Init(TextFile textFile, - StorageFile file, + IStorageFile file, bool resetLastSavedSnapshot = true, bool clearUndoQueue = true, bool isModified = false, @@ -116,7 +116,7 @@ void GetLineColumnSelection( bool IsEditorEnabled(); - Task SaveContentToFileAndUpdateEditorState(StorageFile file); + Task SaveContentToFileAndUpdateEditorState(IStorageFile file); string GetContentForSharing(); diff --git a/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs b/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs index b85923151..15bb46589 100644 --- a/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs +++ b/src/Notepads/Controls/TextEditor/TextEditor.xaml.cs @@ -73,9 +73,9 @@ public sealed partial class TextEditor : ITextEditor, IDisposable public string EditingFilePath { get; private set; } - private StorageFile _editingFile; + private IStorageFile _editingFile; - public StorageFile EditingFile + public IStorageFile EditingFile { get => _editingFile; private set @@ -463,7 +463,7 @@ private KeyboardCommandHandler GetKeyboardCommandHandler() }); } - public void Init(TextFile textFile, StorageFile file, bool resetLastSavedSnapshot = true, bool clearUndoQueue = true, bool isModified = false, bool resetText = true) + public void Init(TextFile textFile, IStorageFile file, bool resetLastSavedSnapshot = true, bool clearUndoQueue = true, bool isModified = false, bool resetText = true) { _loaded = false; EditingFile = file; @@ -709,7 +709,7 @@ public bool IsEditorEnabled() return TextEditorCore.IsEnabled; } - public async Task SaveContentToFileAndUpdateEditorState(StorageFile file) + public async Task SaveContentToFileAndUpdateEditorState(IStorageFile file) { if (Mode == TextEditorMode.DiffPreview) CloseSideBySideDiffViewer(); TextFile textFile = await SaveContentToFile(file); // Will throw if not succeeded @@ -719,7 +719,7 @@ public async Task SaveContentToFileAndUpdateEditorState(StorageFile file) StartCheckingFileStatusPeriodically(); } - private async Task SaveContentToFile(StorageFile file) + private async Task SaveContentToFile(IStorageFile file) { var text = TextEditorCore.GetText(); var encoding = RequestedEncoding ?? LastSavedSnapshot.Encoding; diff --git a/src/Notepads/Core/INotepadsCore.cs b/src/Notepads/Core/INotepadsCore.cs index 3183c3ee8..9bfa79e91 100644 --- a/src/Notepads/Core/INotepadsCore.cs +++ b/src/Notepads/Core/INotepadsCore.cs @@ -33,14 +33,14 @@ public interface INotepadsCore Task CreateTextEditor( Guid id, - StorageFile file, + IStorageFile file, Encoding encoding = null, bool ignoreFileSizeLimit = false); ITextEditor CreateTextEditor( Guid id, TextFile textFile, - StorageFile editingFile, + IStorageFile editingFile, string fileNamePlaceHolder, bool isModified = false); @@ -50,7 +50,7 @@ ITextEditor CreateTextEditor( void OpenTextEditors(ITextEditor[] editors, Guid? selectedEditorId = null); - Task SaveContentToFileAndUpdateEditorState(ITextEditor textEditor, StorageFile file); + Task SaveContentToFileAndUpdateEditorState(ITextEditor textEditor, IStorageFile file); void DeleteTextEditor(ITextEditor textEditor); @@ -72,7 +72,7 @@ ITextEditor CreateTextEditor( ITextEditor GetSelectedTextEditor(); - ITextEditor GetTextEditor(StorageFile file); + ITextEditor GetTextEditor(IStorageFile file); ITextEditor GetTextEditor(string editingFilePath); diff --git a/src/Notepads/Core/NotepadsCore.cs b/src/Notepads/Core/NotepadsCore.cs index 00a81bd3f..52985c46b 100644 --- a/src/Notepads/Core/NotepadsCore.cs +++ b/src/Notepads/Core/NotepadsCore.cs @@ -169,7 +169,7 @@ public void OpenTextEditors(ITextEditor[] editors, Guid? selectedEditorId = null public async Task CreateTextEditor( Guid id, - StorageFile file, + IStorageFile file, Encoding encoding = null, bool ignoreFileSizeLimit = false) { @@ -180,7 +180,7 @@ public async Task CreateTextEditor( public ITextEditor CreateTextEditor( Guid id, TextFile textFile, - StorageFile editingFile, + IStorageFile editingFile, string fileNamePlaceholder, bool isModified = false) { @@ -207,7 +207,7 @@ public ITextEditor CreateTextEditor( return textEditor; } - public async Task SaveContentToFileAndUpdateEditorState(ITextEditor textEditor, StorageFile file) + public async Task SaveContentToFileAndUpdateEditorState(ITextEditor textEditor, IStorageFile file) { await textEditor.SaveContentToFileAndUpdateEditorState(file); // Will throw if not succeeded MarkTextEditorSetSaved(textEditor); @@ -381,7 +381,7 @@ public void CloseTextEditor(ITextEditor textEditor) item?.Close(); } - public ITextEditor GetTextEditor(StorageFile file) + public ITextEditor GetTextEditor(IStorageFile file) { var item = GetTextEditorSetsViewItem(file); return item?.Content as ITextEditor; @@ -427,7 +427,7 @@ private SetsViewItem CreateTextEditorSetsViewItem(ITextEditor textEditor) return textEditorSetsViewItem; } - private SetsViewItem GetTextEditorSetsViewItem(StorageFile file) + private SetsViewItem GetTextEditorSetsViewItem(IStorageFile file) { if (Sets.Items == null) return null; foreach (SetsViewItem setsItem in Sets.Items) @@ -601,7 +601,7 @@ private async void Sets_DragOver(object sender, DragEventArgs args) try { var items = await args.DataView.GetStorageItemsAsync(); - if (items.Count > 0 && items.Any(i => i is StorageFile)) + if (items.Count > 0 && items.Any(i => i is IStorageFile)) { canHandle = true; dragUICaption = _resourceLoader.GetString("App_DragAndDrop_UIOverride_Caption_OpenWithNotepads"); @@ -718,12 +718,12 @@ private async void Sets_Drop(object sender, DragEventArgs args) } } - StorageFile editingFile = null; + IStorageFile editingFile = null; if (metaData.HasEditingFile && args.DataView.Contains(StandardDataFormats.StorageItems)) { var storageItems = await args.DataView.GetStorageItemsAsync(); - if (storageItems.Count == 1 && storageItems[0] is StorageFile file) + if (storageItems.Count == 1 && storageItems[0] is IStorageFile file) { editingFile = file; } diff --git a/src/Notepads/Core/SessionManager.cs b/src/Notepads/Core/SessionManager.cs index 111a039be..233e3af76 100644 --- a/src/Notepads/Core/SessionManager.cs +++ b/src/Notepads/Core/SessionManager.cs @@ -388,7 +388,7 @@ private void UnbindEditorContentStateChangeEvent(object sender, ITextEditor text private async Task RecoverTextEditorAsync(TextEditorSessionDataV1 editorSessionData) { - StorageFile editingFile = null; + IStorageFile editingFile = null; if (editorSessionData.EditingFileFutureAccessToken != null) { @@ -448,7 +448,7 @@ private async Task RecoverTextEditorAsync(TextEditorSessionDataV1 e return textEditor; } - private static async Task BackupTextAsync(string text, Encoding encoding, LineEnding lineEnding, StorageFile file) + private static async Task BackupTextAsync(string text, Encoding encoding, LineEnding lineEnding, IStorageFile file) { try { @@ -470,7 +470,7 @@ private async Task DeleteOrphanedBackupFilesAsync(NotepadsSessionDataV1 sessionD .Where(path => path != null) .ToHashSet(StringComparer.OrdinalIgnoreCase); - foreach (StorageFile backupFile in await SessionUtility.GetAllBackupFilesAsync(_backupFolderName)) + foreach (var backupFile in await SessionUtility.GetAllBackupFilesAsync(_backupFolderName)) { if (!backupPaths.Contains(backupFile.Path)) { diff --git a/src/Notepads/Notepads.csproj b/src/Notepads/Notepads.csproj index 6615d3aa4..e5fed029d 100644 --- a/src/Notepads/Notepads.csproj +++ b/src/Notepads/Notepads.csproj @@ -11,6 +11,7 @@ Notepads en-US UAP + true 10.0.19041.0 10.0.17763.0 14 @@ -63,17 +64,53 @@ true true + + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP;DISABLE_XAML_GENERATED_MAIN + false + ;2008 + full + ARM + false + prompt + true + + + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP;DISABLE_XAML_GENERATED_MAIN + true + ;2008 + pdbonly + ARM + false + prompt + true + true + + + bin\ARM\Production\ + TRACE;NETFX_CORE;WINDOWS_UWP;DISABLE_XAML_GENERATED_MAIN + true + ;2008 + pdbonly + ARM + false + prompt + true + true + true bin\ARM64\Debug\ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP;DISABLE_XAML_GENERATED_MAIN + false ;2008 full ARM64 false prompt true - true bin\ARM64\Release\ @@ -175,6 +212,7 @@ + @@ -462,7 +500,7 @@ - Scale|DXFeatureLevel - + @@ -32,7 +32,7 @@ - + diff --git a/src/Notepads/Properties/AssemblyInfo.cs b/src/Notepads/Properties/AssemblyInfo.cs index 70206ed35..d62ab1e21 100644 --- a/src/Notepads/Properties/AssemblyInfo.cs +++ b/src/Notepads/Properties/AssemblyInfo.cs @@ -1,7 +1,8 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Notepads")] @@ -16,13 +17,14 @@ // Version information for an assembly consists of the following four values: // // Major Version -// Minor Version +// Minor Version // Build Number // Revision // -// You can specify all the values or you can default the Build and Revision Numbers +// You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: ComVisible(false)] \ No newline at end of file +[assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("Notepads.UnitTest")] \ No newline at end of file diff --git a/src/Notepads/Services/ActivationService.cs b/src/Notepads/Services/ActivationService.cs index dcb8fbea6..a7b340f92 100644 --- a/src/Notepads/Services/ActivationService.cs +++ b/src/Notepads/Services/ActivationService.cs @@ -90,7 +90,7 @@ private static async Task CommandActivated(Frame rootFrame, CommandLineActivated } else if (rootFrame.Content is NotepadsMainPage mainPage) { - var file = await FileSystemUtility.OpenFileFromCommandLine( + var file = await CommandLineUtility.OpenFileFromCommandLine( commandLineActivatedEventArgs.Operation.CurrentDirectoryPath, commandLineActivatedEventArgs.Operation.Arguments); diff --git a/src/Notepads/Services/LoggingService.cs b/src/Notepads/Services/LoggingService.cs index 5c291013d..f5f237bb1 100644 --- a/src/Notepads/Services/LoggingService.cs +++ b/src/Notepads/Services/LoggingService.cs @@ -20,7 +20,7 @@ public static class LoggingService private static readonly TimeSpan LoggingInterval = TimeSpan.FromSeconds(10); private static readonly List Messages = new List(); - private static StorageFile _logFile; + private static IStorageFile _logFile; private static Task _backgroundTask; private static bool _initialized; @@ -37,7 +37,7 @@ public static async Task InitializeFileSystemLoggingAsync() await InitializeLogFileWriterBackgroundTaskAsync(); } - public static StorageFile GetLogFile() + public static IStorageFile GetLogFile() { return _logFile; } diff --git a/src/Notepads/Utilities/CommandLineUtility.cs b/src/Notepads/Utilities/CommandLineUtility.cs new file mode 100644 index 000000000..c19c830a6 --- /dev/null +++ b/src/Notepads/Utilities/CommandLineUtility.cs @@ -0,0 +1,150 @@ +namespace Notepads.Utilities +{ + using System; + using System.IO; + using System.Runtime.InteropServices; + using System.Threading.Tasks; + using Microsoft.Win32; + using Notepads.Services; + using Windows.Storage; + using Windows.System; + + public static class CommandLineUtility + { + private static bool _hasSetEnvironmentVariables = false; + + public static bool IsFullPath(string path) + { + return !string.IsNullOrWhiteSpace(path) + && path.IndexOfAny(Path.GetInvalidPathChars()) == -1 + && Path.IsPathRooted(path) + && !Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal); + } + + public static string GetAbsolutePath(string basePath, string path) + { + // Resolves any internal "..\" to get the true full path. + return Path.GetFullPath( + Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) + ? Path.Combine(Path.GetPathRoot(basePath), path.TrimStart(Path.DirectorySeparatorChar)) + : Path.Combine(basePath, path)); + } + + public static async Task OpenFileFromCommandLine(string dir, string args) + { + SetEnvironmentVariables(); + + string path = null; + try + { + path = GetAbsolutePathFromCommandLine(dir, args); + } + catch (Exception ex) + { + LoggingService.LogError($"[{nameof(FileSystemUtility)}] Failed to parse command line: {args} with Exception: {ex}"); + } + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + LoggingService.LogInfo($"[{nameof(FileSystemUtility)}] OpenFileFromCommandLine: {path}"); + + var result = await FileSystemUtility.GetFile(path); + if (result.Item2 != null) + { + DispatcherQueue.GetForCurrentThread().TryEnqueue( + () => NotificationCenter.Instance.PostNotification(result.Item2.Message, 1500) + ); + } + return result.Item1; + } + + private static void SetEnvironmentVariables() + { + if (_hasSetEnvironmentVariables) return; + + Environment.SetEnvironmentVariable( + "homepath", + UserDataPaths.GetDefault().Profile); + + Environment.SetEnvironmentVariable( + "localappdata", + UserDataPaths.GetDefault().LocalAppData); + + Environment.SetEnvironmentVariable( + "temp", + (string)Registry.GetValue( + @"HKEY_CURRENT_USER\Environment", + "TEMP", + Environment.GetEnvironmentVariable("temp"))); + + Environment.SetEnvironmentVariable( + "tmp", + (string)Registry.GetValue( + @"HKEY_CURRENT_USER\Environment", + "TEMP", + Environment.GetEnvironmentVariable("tmp"))); + + _hasSetEnvironmentVariables = true; + } + + internal static string GetAbsolutePathFromCommandLine(string dir, string args) + { + if (string.IsNullOrEmpty(args)) return null; + + var path = Environment.ExpandEnvironmentVariables( + RemoveExecutableNameOrPathFromCommandLineArgs(args.Trim())); + + if (string.IsNullOrEmpty(path)) + { + return null; + } + + // Replace all forward slash with platform supported directory separator + path = path.Replace('/', Path.DirectorySeparatorChar); + + if (IsFullPath(path)) + { + return path; + } + + path = GetAbsolutePath(dir, path); + + return path; + } + + private static string RemoveExecutableNameOrPathFromCommandLineArgs(string args) + { + var argv = CommandLineToArgvW(args, out int argc); + if (argc <= 1) return null; + + var argsBuilder = new string[argc - 1]; + try + { + for (var i = 0; i < argsBuilder.Length; i++) + { + argsBuilder[i] = Marshal.PtrToStringUni(Marshal.ReadIntPtr(argv, (i + 1) * IntPtr.Size)); + } + } + finally + { + Marshal.FreeHGlobal(argv); + } + return string.Join(' ', argsBuilder).Trim(); + } + + #region Win32 and COM APIs + + [DllImport("api-ms-win-shcore-obsolete-l1-1-0.dll", CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private unsafe static extern IntPtr CommandLineToArgvW( + [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, + out int pNumArgs + ); + + #endregion + } +} \ No newline at end of file diff --git a/src/Notepads/Utilities/FileSystemUtility.cs b/src/Notepads/Utilities/FileSystemUtility.cs index 833693334..037240715 100644 --- a/src/Notepads/Utilities/FileSystemUtility.cs +++ b/src/Notepads/Utilities/FileSystemUtility.cs @@ -12,7 +12,6 @@ using Notepads.Services; using Windows.ApplicationModel.Resources; using Windows.Storage; - using Windows.Storage.FileProperties; using Windows.Storage.Provider; using UtfUnknown; @@ -29,12 +28,12 @@ public enum InvalidFilenameError public static class FileSystemUtility { - private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForCurrentView(); - - private const string WslRootPath = "\\\\wsl$\\"; + private static readonly ResourceLoader _resourceLoader = ResourceLoader.GetForCurrentView(); // https://stackoverflow.com/questions/62771/how-do-i-check-if-a-given-string-is-a-legal-valid-file-name-under-windows - private static readonly Regex ValidWindowsFileNames = new Regex(@"^(?!(?:PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d)(?:\..+)?$)[^\x00-\x1F\xA5\\?*:\"";|\/<>]+(?]+(? OpenFileFromCommandLine(string dir, string args) - { - string path = null; - - try - { - args = ReplaceEnvironmentVariables(args); - path = GetAbsolutePathFromCommandLine(dir, args, App.ApplicationName); - } - catch (Exception ex) - { - LoggingService.LogError($"[{nameof(FileSystemUtility)}] Failed to parse command line: {args} with Exception: {ex}"); - } - - if (string.IsNullOrEmpty(path)) - { - return null; - } - - LoggingService.LogInfo($"[{nameof(FileSystemUtility)}] OpenFileFromCommandLine: {path}"); - - return await GetFile(path); - } - - private static string ReplaceEnvironmentVariables(string args) - { - if (args.Contains("%homepath%", StringComparison.OrdinalIgnoreCase)) - { - args = args.Replace("%homepath%", - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - StringComparison.OrdinalIgnoreCase); - } - - if (args.Contains("%localappdata%", StringComparison.OrdinalIgnoreCase)) - { - args = args.Replace("%localappdata%", - UserDataPaths.GetDefault().LocalAppData, - StringComparison.OrdinalIgnoreCase); - } - - if (args.Contains("%temp%", StringComparison.OrdinalIgnoreCase)) - { - args = args.Replace("%temp%", - (string)Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Environment", - "TEMP", - Environment.GetEnvironmentVariable("temp")), - StringComparison.OrdinalIgnoreCase); - } - - if (args.Contains("%tmp%", StringComparison.OrdinalIgnoreCase)) - { - args = args.Replace("%tmp%", - (string)Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Environment", - "TEMP", - Environment.GetEnvironmentVariable("tmp")), - StringComparison.OrdinalIgnoreCase); - } - - return Environment.ExpandEnvironmentVariables(args); - } - - public static string GetAbsolutePathFromCommandLine(string dir, string args, string appName) - { - if (string.IsNullOrEmpty(args)) return null; - - args = args.Trim(); - - args = RemoveExecutableNameOrPathFromCommandLineArgs(args, appName); - - if (string.IsNullOrEmpty(args)) - { - return null; - } - - string path = args; - - // Get first quoted string if any - if (path.StartsWith("\"") && path.Length > 1) - { - var index = path.IndexOf('\"', 1); - if (index == -1) return null; - path = args.Substring(1, index - 1); - } - - if (dir.StartsWith(WslRootPath)) - { - if (path.StartsWith('/')) - { - var distroRootPath = dir.Substring(0, dir.IndexOf('\\', WslRootPath.Length) + 1); - var fullPath = distroRootPath + path.Trim('/').Replace('/', Path.DirectorySeparatorChar); - if (IsFullPath(fullPath)) return fullPath; - } - } - - // Replace all forward slash with platform supported directory separator - path = path.Trim('/').Replace('/', Path.DirectorySeparatorChar); - - if (IsFullPath(path)) - { - return path; - } - - if (path.StartsWith(".\\")) - { - path = dir + Path.DirectorySeparatorChar + path.Substring(2, path.Length - 2); - } - else if (path.StartsWith("..\\")) - { - path = GetAbsolutePath(dir, path); - } - else - { - path = dir + Path.DirectorySeparatorChar + path; - } - - return path; - } - - private static string RemoveExecutableNameOrPathFromCommandLineArgs(string args, string appName) - { - if (!args.StartsWith('\"')) - { - // From Windows Command Line - // notepads ... - // notepads.exe - - if (args.StartsWith($"{appName}.exe", - StringComparison.OrdinalIgnoreCase)) - { - args = args.Substring($"{appName}.exe".Length); - } - - if (args.StartsWith(appName, - StringComparison.OrdinalIgnoreCase)) - { - args = args.Substring(appName.Length); - } - } - else if (args.StartsWith('\"') && args.Length > 1) - { - // From PowerShell or run - // "notepads" - // "notepads.exe" - // ".exe" ... - var index = args.IndexOf('\"', 1); - if (index == -1) return null; - if (args.Length == index + 1) return null; - args = args.Substring(index + 1); - } - else - { - return null; - } - - args = args.Trim(); - return args; - } - - public static async Task GetFileProperties(StorageFile file) - { - return await file.GetBasicPropertiesAsync(); - } - - public static async Task GetDateModified(StorageFile file) + public static async Task GetDateModified(IStorageFile file) { - var properties = await GetFileProperties(file); - var dateModified = properties.DateModified; - return dateModified.ToFileTime(); + var properties = await file.GetBasicPropertiesAsync(); + return properties.DateModified.ToFileTime(); } - public static bool IsFileReadOnly(StorageFile file) + public static bool IsFileReadOnly(IStorageFile file) { return (file.Attributes & Windows.Storage.FileAttributes.ReadOnly) != 0; } - public static async Task IsFileWritable(StorageFile file) + public static async Task IsFileWritable(IStorageFile file) { try { @@ -300,31 +104,31 @@ public static async Task IsFileWritable(StorageFile file) } } - public static async Task GetFile(string filePath) + public static async Task> GetFile(string filePath) { try { - return await StorageFile.GetFileFromPathAsync(filePath); + return new Tuple(await StorageFile.GetFileFromPathAsync(filePath), null); } - catch + catch (Exception e) { - return null; + return new Tuple(null, e); } } public static async Task ReadFile(string filePath, bool ignoreFileSizeLimit, Encoding encoding) { - StorageFile file = await GetFile(filePath); + var file = (await GetFile(filePath)).Item1; return file == null ? null : await ReadFile(file, ignoreFileSizeLimit, encoding); } - public static async Task ReadFile(StorageFile file, bool ignoreFileSizeLimit, Encoding encoding = null) + public static async Task ReadFile(IStorageFile file, bool ignoreFileSizeLimit, Encoding encoding = null) { var fileProperties = await file.GetBasicPropertiesAsync(); if (!ignoreFileSizeLimit && fileProperties.Size > 1000 * 1024) { - throw new Exception(ResourceLoader.GetString("ErrorMessage_NotepadsFileSizeLimit")); + throw new Exception(_resourceLoader.GetString("ErrorMessage_NotepadsFileSizeLimit")); } Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); @@ -545,7 +349,7 @@ private static Encoding FixUtf8Bom(Encoding encoding, byte[] bom) /// /// /// - public static async Task WriteToFile(string text, Encoding encoding, StorageFile file) + public static async Task WriteToFile(string text, Encoding encoding, IStorageFile file) { bool usedDeferUpdates = true; @@ -575,7 +379,7 @@ public static async Task WriteToFile(string text, Encoding encoding, StorageFile var result = encoding.GetPreamble().Concat(content).ToArray(); await PathIO.WriteBytesAsync(file.Path, result); } - else // Use StorageFile API to save + else // Use StorageFile API to save { using (var stream = await file.OpenStreamForWriteAsync()) using (var writer = new StreamWriter(stream, encoding)) @@ -611,7 +415,7 @@ internal static async Task DeleteFile(string filePath, StorageDeleteOption delet { try { - var file = await GetFile(filePath); + var file = (await GetFile(filePath)).Item1; if (file != null) { await file.DeleteAsync(deleteOption); @@ -629,12 +433,12 @@ public static async Task GetOrCreateAppFolder(string folderName) return await localFolder.CreateFolderAsync(folderName, CreationCollisionOption.OpenIfExists); } - public static async Task CreateFile(StorageFolder folder, string fileName, CreationCollisionOption option = CreationCollisionOption.ReplaceExisting) + public static async Task CreateFile(StorageFolder folder, string fileName, CreationCollisionOption option = CreationCollisionOption.ReplaceExisting) { return await folder.CreateFileAsync(fileName, option); } - public static async Task FileExists(StorageFile file) + public static async Task FileExists(IStorageFile file) { try { diff --git a/src/Notepads/Utilities/FutureAccessListUtility.cs b/src/Notepads/Utilities/FutureAccessListUtility.cs index eeb3bd4f0..ffbe8d099 100644 --- a/src/Notepads/Utilities/FutureAccessListUtility.cs +++ b/src/Notepads/Utilities/FutureAccessListUtility.cs @@ -10,7 +10,7 @@ public static class FutureAccessListUtility { - public static async Task GetFileFromFutureAccessList(string token) + public static async Task GetFileFromFutureAccessList(string token) { try { @@ -26,7 +26,7 @@ public static async Task GetFileFromFutureAccessList(string token) return null; } - public static async Task TryAddOrReplaceTokenInFutureAccessList(string token, StorageFile file) + public static async Task TryAddOrReplaceTokenInFutureAccessList(string token, IStorageFile file) { try { diff --git a/src/Notepads/Utilities/SessionUtility.cs b/src/Notepads/Utilities/SessionUtility.cs index 8aa9e5061..e7aeca4e1 100644 --- a/src/Notepads/Utilities/SessionUtility.cs +++ b/src/Notepads/Utilities/SessionUtility.cs @@ -50,7 +50,7 @@ public static async Task GetBackupFolderAsync(string backupFolder return await FileSystemUtility.GetOrCreateAppFolder(backupFolderName); } - public static async Task> GetAllBackupFilesAsync(string backupFolderName) + public static async Task> GetAllBackupFilesAsync(string backupFolderName) { StorageFolder backupFolder = await GetBackupFolderAsync(backupFolderName); return await backupFolder.GetFilesAsync(); @@ -108,7 +108,7 @@ public static async Task DeleteSerializedSessionMetaDataAsync(string sessionMeta } } - public static async Task CreateNewFileInBackupFolderAsync(string fileName, CreationCollisionOption collisionOption, string backupFolderName) + public static async Task CreateNewFileInBackupFolderAsync(string fileName, CreationCollisionOption collisionOption, string backupFolderName) { StorageFolder backupFolder = await GetBackupFolderAsync(backupFolderName); return await backupFolder.CreateFileAsync(fileName, collisionOption); diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs index 81d0e8b2f..3b9ecb9c8 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.IO.cs @@ -18,7 +18,7 @@ public sealed partial class NotepadsMainPage { private async Task OpenNewFiles() { - IReadOnlyList files; + IReadOnlyList files; try { @@ -47,7 +47,7 @@ private async Task OpenNewFiles() } } - public async Task OpenFile(StorageFile file, bool rebuildOpenRecentItems = true) + public async Task OpenFile(IStorageFile file, bool rebuildOpenRecentItems = true) { try { @@ -90,7 +90,7 @@ public async Task OpenFile(StorageFile file, bool rebuildOpenRecentItems = // This information will be used to on-board new extension support for future release. // Because UWP does not allow user to associate arbitrary file extension with the app. // File name will not and should not be tracked. - private void TrackFileExtensionIfNotSupported(StorageFile file) + private void TrackFileExtensionIfNotSupported(IStorageFile file) { try { @@ -119,7 +119,7 @@ public async Task OpenFiles(IReadOnlyList storageItems) int successCount = 0; foreach (var storageItem in storageItems) { - if (storageItem is StorageFile file) + if (storageItem is IStorageFile file) { if (await OpenFile(file, rebuildOpenRecentItems: false)) { @@ -134,15 +134,15 @@ public async Task OpenFiles(IReadOnlyList storageItems) return successCount; } - private async Task OpenFileUsingFileSavePicker(ITextEditor textEditor) + private async Task OpenFileUsingFileSavePicker(ITextEditor textEditor) { NotepadsCore.SwitchTo(textEditor); - StorageFile file = await FilePickerFactory.GetFileSavePicker(textEditor).PickSaveFileAsync(); + var file = await FilePickerFactory.GetFileSavePicker(textEditor).PickSaveFileAsync(); NotepadsCore.FocusOnTextEditor(textEditor); return file; } - private async Task SaveInternal(ITextEditor textEditor, StorageFile file, bool rebuildOpenRecentItems) + private async Task SaveInternal(ITextEditor textEditor, IStorageFile file, bool rebuildOpenRecentItems) { await NotepadsCore.SaveContentToFileAndUpdateEditorState(textEditor, file); var success = MRUService.TryAdd(file); // Remember recently used files @@ -161,7 +161,7 @@ private async Task Save(ITextEditor textEditor, bool saveAs, bool ignoreUn return true; } - StorageFile file = null; + IStorageFile file = null; try { diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs index df903eb93..72b22200e 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.MainMenu.cs @@ -119,7 +119,7 @@ private async Task BuildOpenRecentButtonSubItems() foreach (var item in await MRUService.Get(top: 10)) { - if (item is StorageFile file) + if (item is IStorageFile file) { if (MRUFileList.Contains(file.Path)) { diff --git a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs index 7badfc983..cbd4949c9 100644 --- a/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs +++ b/src/Notepads/Views/MainPage/NotepadsMainPage.xaml.cs @@ -170,7 +170,7 @@ private static async Task OpenNewAppInstance() } } - #region Application Life Cycle & Window management + #region Application Life Cycle & Window management // Handles external links or cmd args activation before Sets loaded protected override void OnNavigatedTo(NavigationEventArgs e) @@ -220,7 +220,7 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) } else if (_appLaunchCmdDir != null) { - var file = await FileSystemUtility.OpenFileFromCommandLine(_appLaunchCmdDir, _appLaunchCmdArgs); + var file = await CommandLineUtility.OpenFileFromCommandLine(_appLaunchCmdDir, _appLaunchCmdArgs); if (file != null && await OpenFile(file)) { loadedCount++; @@ -265,7 +265,7 @@ private async void Sets_Loaded(object sender, RoutedEventArgs e) // An issue with the Game Bar extension model and Windows platform prevents the Notepads process from exiting cleanly // when more than one CoreWindow has been created, and NotepadsMainPage is the last to close. The common case for this // is to open Notepads in Game Bar, then open its settings, then close the settings and finally close Notepads. - // This puts the process in a bad state where it will no longer open in Game Bar and the Notepads process is orphaned. + // This puts the process in a bad state where it will no longer open in Game Bar and the Notepads process is orphaned. // To work around this do not use the EnteredBackground event when running as a widget. // Microsoft is tracking this issue as VSO#25735260 Application.Current.EnteredBackground -= App_EnteredBackground; @@ -586,7 +586,7 @@ private async void OnStorageItemsDropped(object sender, IReadOnlyList