From 5d7c8b90e1a08940c7bee078db130c79a91a478a Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Mon, 21 Sep 2015 14:02:56 -0700 Subject: [PATCH 1/2] Copy/paste REPL clipboard format --- src/InteractiveWindow/Editor/BufferBlock.cs | 49 +++++++ .../Editor/InteractiveWindow.ReplSpanKind.cs | 53 ++++---- .../Editor/InteractiveWindow.UIThreadOnly.cs | 96 ++++++++++---- .../Editor/InteractiveWindow.cs | 2 + .../Editor/InteractiveWindow.csproj | 4 +- .../EditorTest/InteractiveWindowTest.csproj | 2 +- .../EditorTest/InteractiveWindowTests.cs | 122 ++++++++++++++---- .../EditorTest/TestInteractiveEngine.cs | 2 +- 8 files changed, 252 insertions(+), 78 deletions(-) create mode 100644 src/InteractiveWindow/Editor/BufferBlock.cs diff --git a/src/InteractiveWindow/Editor/BufferBlock.cs b/src/InteractiveWindow/Editor/BufferBlock.cs new file mode 100644 index 0000000000000..6000157c9ca47 --- /dev/null +++ b/src/InteractiveWindow/Editor/BufferBlock.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace Microsoft.VisualStudio.InteractiveWindow +{ + /// + /// REPL session buffer: input, output, or prompt. + /// + [DataContract] + internal struct BufferBlock + { + [DataMember(Name = "kind")] + internal readonly ReplSpanKind Kind; + + [DataMember(Name = "content")] + internal readonly string Content; + + internal BufferBlock(ReplSpanKind kind, string content) + { + Kind = kind; + Content = content; + } + + internal static string Serialize(BufferBlock[] blocks) + { + var serializer = new DataContractJsonSerializer(typeof(BufferBlock[])); + using (var stream = new MemoryStream()) + { + serializer.WriteObject(stream, blocks); + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length); + } + } + + internal static BufferBlock[] Deserialize(string str) + { + var serializer = new DataContractJsonSerializer(typeof(BufferBlock[])); + var bytes = Encoding.UTF8.GetBytes(str); + using (var stream = new MemoryStream(bytes)) + { + var obj = serializer.ReadObject(stream); + return (BufferBlock[])obj; + } + } + } +} diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.ReplSpanKind.cs b/src/InteractiveWindow/Editor/InteractiveWindow.ReplSpanKind.cs index 290df7524f9b8..d9244c56c4587 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.ReplSpanKind.cs +++ b/src/InteractiveWindow/Editor/InteractiveWindow.ReplSpanKind.cs @@ -1,35 +1,40 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Runtime.Serialization; + namespace Microsoft.VisualStudio.InteractiveWindow { - internal partial class InteractiveWindow + [DataContract] + internal enum ReplSpanKind { - private enum ReplSpanKind - { - /// - /// Primary, secondary, or standard input prompt. - /// - Prompt, + /// + /// Primary, secondary, or standard input prompt. + /// + [EnumMember] + Prompt = 0, - /// - /// Line break inserted at end of output. - /// - LineBreak, + /// + /// The span represents output from the program (standard output). + /// + [EnumMember] + Output = 1, - /// - /// The span represents output from the program (standard output). - /// - Output, + /// + /// The span represents code inputted after a prompt or secondary prompt. + /// + [EnumMember] + Input = 2, - /// - /// The span represents code inputted after a prompt or secondary prompt. - /// - Language, + /// + /// The span represents the input for a standard input (non code input). + /// + [EnumMember] + StandardInput = 3, - /// - /// The span represents the input for a standard input (non code input). - /// - StandardInput, - } + /// + /// Line break inserted at end of output. + /// + [EnumMember] + LineBreak = 4, } } \ No newline at end of file diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs b/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs index e0d5f8cd76900..00f7897e84d39 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs +++ b/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs @@ -213,7 +213,7 @@ public async Task ResetAsync(bool initialize) { var snapshot = _projectionBuffer.CurrentSnapshot; var spanCount = snapshot.SpanCount; - Debug.Assert(GetSpanKind(snapshot.GetSourceSpan(spanCount - 1)) == ReplSpanKind.Language); + Debug.Assert(GetSpanKind(snapshot.GetSourceSpan(spanCount - 1)) == ReplSpanKind.Input); StoreUncommittedInput(); RemoveProjectionSpans(spanCount - 2, 2); CurrentLanguageBuffer = null; @@ -356,7 +356,7 @@ public async Task ReadStandardInputAsync() { var snapshot = _projectionBuffer.CurrentSnapshot; var spanCount = snapshot.SpanCount; - if (spanCount > 0 && GetSpanKind(snapshot.GetSourceSpan(spanCount - 1)) == ReplSpanKind.Language) + if (spanCount > 0 && GetSpanKind(snapshot.GetSourceSpan(spanCount - 1)) == ReplSpanKind.Input) { // we need to remove our input prompt. RemoveLastInputPrompt(); @@ -584,6 +584,22 @@ public bool Paste() { InsertCode(format); } + else if (Clipboard.ContainsData(ClipboardFormat)) + { + var blocks = BufferBlock.Deserialize((string)Clipboard.GetData(ClipboardFormat)); + // Paste each block separately. + foreach (var block in blocks) + { + switch (block.Kind) + { + case ReplSpanKind.Input: + case ReplSpanKind.Output: + case ReplSpanKind.StandardInput: + InsertCode(block.Content); + break; + } + } + } else if (Clipboard.ContainsText()) { InsertCode(Clipboard.GetText()); @@ -657,7 +673,7 @@ private void AppendInput(string text) var snapshot = _projectionBuffer.CurrentSnapshot; var spanCount = snapshot.SpanCount; var inputSpan = snapshot.GetSourceSpan(spanCount - 1); - Debug.Assert(GetSpanKind(inputSpan) == ReplSpanKind.Language || + Debug.Assert(GetSpanKind(inputSpan) == ReplSpanKind.Input || GetSpanKind(inputSpan) == ReplSpanKind.StandardInput); var buffer = inputSpan.Snapshot.TextBuffer; @@ -1094,7 +1110,7 @@ private ITextBuffer GetLanguageBuffer(SnapshotPoint point) // Grab the span following the prompt (either language or standard input). var projectionSpan = sourceSpans[promptIndex + 1]; var kind = GetSpanKind(projectionSpan); - if (kind != ReplSpanKind.Language) + if (kind != ReplSpanKind.Input) { Debug.Assert(kind == ReplSpanKind.StandardInput); return null; @@ -1535,7 +1551,7 @@ private int GetProjectionSpanIndexFromEditableBufferPosition(IProjectionSnapshot // and ending at the end of the projection buffer, each language buffer projection is on a separate line: // [prompt)[language)...[prompt)[language) int result = projectionSpansCount - (surfaceSnapshot.LineCount - surfaceLineNumber) * SpansPerLineOfInput + 1; - Debug.Assert(GetSpanKind(surfaceSnapshot.GetSourceSpan(result)) == ReplSpanKind.Language); + Debug.Assert(GetSpanKind(surfaceSnapshot.GetSourceSpan(result)) == ReplSpanKind.Input); return result; } @@ -2027,11 +2043,11 @@ public void SelectAll() var inputSnapshot = projectionSpan.Snapshot; var kind = GetSpanKind(projectionSpan); - Debug.Assert(kind == ReplSpanKind.Language || kind == ReplSpanKind.StandardInput); + Debug.Assert(kind == ReplSpanKind.Input || kind == ReplSpanKind.StandardInput); // Language input block is a projection of the entire snapshot; // std input block is a projection of a single span: - SnapshotPoint inputBufferEnd = (kind == ReplSpanKind.Language) ? + SnapshotPoint inputBufferEnd = (kind == ReplSpanKind.Input) ? new SnapshotPoint(inputSnapshot, inputSnapshot.Length) : projectionSpan.End; @@ -2359,17 +2375,45 @@ private static NormalizedSnapshotSpanCollection GetSelectionSpans(ITextView text private DataObject Copy(NormalizedSnapshotSpanCollection spans) { - var text = spans.Aggregate(new StringBuilder(), GetTextWithoutPrompts, b => b.ToString()); + var text = GetText(spans); + var blocks = GetTextBlocks(spans); var rtf = _rtfBuilderService.GenerateRtf(spans, TextView); var data = new DataObject(); data.SetData(DataFormats.StringFormat, text); data.SetData(DataFormats.Text, text); data.SetData(DataFormats.UnicodeText, text); data.SetData(DataFormats.Rtf, rtf); + data.SetData(ClipboardFormat, blocks); return data; } - private StringBuilder GetTextWithoutPrompts(StringBuilder builder, SnapshotSpan span) + /// + /// Get the text of the given spans as a simple concatenated string. + /// + private static string GetText(NormalizedSnapshotSpanCollection spans) + { + var builder = new StringBuilder(); + foreach (var span in spans) + { + builder.Append(span.GetText()); + } + return builder.ToString(); + } + + /// + /// Get the text of the given spans as a serialized BufferBlock[]. + /// + private string GetTextBlocks(NormalizedSnapshotSpanCollection spans) + { + var blocks = new List(); + foreach (var span in spans) + { + GetTextBlocks(blocks, span); + } + return BufferBlock.Serialize(blocks.ToArray()); + } + + private void GetTextBlocks(List blocks, SnapshotSpan span) { // Find the range of source spans that cover the span. var sourceSpans = GetSourceSpans(span.Snapshot); @@ -2380,7 +2424,6 @@ private StringBuilder GetTextWithoutPrompts(StringBuilder builder, SnapshotSpan index--; } - // Add the text for all non-prompt spans within the range. for (; index < n; index++) { var sourceSpan = sourceSpans[index]; @@ -2388,28 +2431,29 @@ private StringBuilder GetTextWithoutPrompts(StringBuilder builder, SnapshotSpan { continue; } - if (!IsPrompt(sourceSpan)) + var sourceSnapshot = sourceSpan.Snapshot; + var mappedSpans = TextView.BufferGraph.MapDownToBuffer(span, SpanTrackingMode.EdgeExclusive, sourceSnapshot.TextBuffer); + bool added = false; + foreach (var mappedSpan in mappedSpans) { - var sourceSnapshot = sourceSpan.Snapshot; - var mappedSpans = TextView.BufferGraph.MapDownToBuffer(span, SpanTrackingMode.EdgeExclusive, sourceSnapshot.TextBuffer); - bool added = false; - foreach (var mappedSpan in mappedSpans) + var intersection = sourceSpan.Span.Intersection(mappedSpan); + if (intersection.HasValue && !intersection.Value.IsEmpty) { - var intersection = sourceSpan.Span.Intersection(mappedSpan); - if (intersection.HasValue) + var kind = GetSpanKind(span); + if (kind == ReplSpanKind.LineBreak) { - builder.Append(sourceSnapshot.GetText(intersection.Value)); - added = true; + kind = ReplSpanKind.Output; } + var content = sourceSnapshot.GetText(intersection.Value); + blocks.Add(new BufferBlock(kind, content)); + added = true; } - if (!added) - { - break; - } + } + if (!added) + { + break; } } - - return builder; } /// Implements . @@ -2689,7 +2733,7 @@ private ReplSpanKind GetSpanKind(SnapshotSpan span) ReplSpanKind.LineBreak : ReplSpanKind.Prompt; } - return ReplSpanKind.Language; + return ReplSpanKind.Input; } #region Output diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.cs b/src/InteractiveWindow/Editor/InteractiveWindow.cs index 614e602ac8e78..7734043951123 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.cs +++ b/src/InteractiveWindow/Editor/InteractiveWindow.cs @@ -33,6 +33,8 @@ namespace Microsoft.VisualStudio.InteractiveWindow /// internal partial class InteractiveWindow : IInteractiveWindow, IInteractiveWindowOperations2 { + internal const string ClipboardFormat = "89344A36-9821-495A-8255-99A63969F87D"; + public event EventHandler SubmissionBufferAdded; PropertyCollection IPropertyOwner.Properties { get; } = new PropertyCollection(); diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.csproj b/src/InteractiveWindow/Editor/InteractiveWindow.csproj index 7aca38e044b6e..9ad342319a378 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.csproj +++ b/src/InteractiveWindow/Editor/InteractiveWindow.csproj @@ -56,8 +56,7 @@ - - + @@ -66,6 +65,7 @@ + diff --git a/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj b/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj index 2348875638c0f..b1d020ce96c0c 100644 --- a/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj +++ b/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj @@ -96,4 +96,4 @@ - + \ No newline at end of file diff --git a/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs b/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs index f244befcbe12d..fbcc74ca5927a 100644 --- a/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs +++ b/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Windows; using Microsoft.VisualStudio.InteractiveWindow.Commands; @@ -586,7 +587,7 @@ public void CopyWithinInput() Window.InsertCode("1 + 2"); Window.Operations.SelectAll(); Window.Operations.Copy(); - VerifyClipboardData("1 + 2"); + VerifyClipboardData("1 + 2", "1 + 2", @"[{""content"":""1 + 2"",""kind"":2}]"); // Shrink the selection. var selection = Window.TextView.Selection; @@ -594,7 +595,7 @@ public void CopyWithinInput() selection.Select(new SnapshotSpan(span.Snapshot, span.Start + 1, span.Length - 2), isReversed: false); Window.Operations.Copy(); - VerifyClipboardData(" + "); + VerifyClipboardData(" + ", " + ", @"[{""content"":"" + "",""kind"":2}]"); } [Fact] @@ -616,13 +617,14 @@ public void CopyInputAndOutput() Window.Operations.SelectAll(); Window.Operations.SelectAll(); Window.Operations.Copy(); - VerifyClipboardData(@"foreach (var o in new[] { 1, 2, 3 }) -System.Console.WriteLine(); + VerifyClipboardData(@"> foreach (var o in new[] { 1, 2, 3 }) +> System.Console.WriteLine(); 1 2 3 -", -@"> foreach (var o in new[] \{ 1, 2, 3 \})\par > System.Console.WriteLine();\par 1\par 2\par 3\par > "); +> ", +@"> foreach (var o in new[] \{ 1, 2, 3 \})\par > System.Console.WriteLine();\par 1\par 2\par 3\par > ", +@"[{""content"":""> "",""kind"":2},{""content"":""foreach (var o in new[] { 1, 2, 3 })\u000d\u000a"",""kind"":2},{""content"":""> "",""kind"":2},{""content"":""System.Console.WriteLine();\u000d\u000a"",""kind"":2},{""content"":""1\u000d\u000a2\u000d\u000a3\u000d\u000a"",""kind"":2},{""content"":""> "",""kind"":2}]"); // Shrink the selection. var selection = Window.TextView.Selection; @@ -631,11 +633,12 @@ public void CopyInputAndOutput() Window.Operations.Copy(); VerifyClipboardData(@"oreach (var o in new[] { 1, 2, 3 }) -System.Console.WriteLine(); +> System.Console.WriteLine(); 1 2 3", -@"oreach (var o in new[] \{ 1, 2, 3 \})\par > System.Console.WriteLine();\par 1\par 2\par 3"); +@"oreach (var o in new[] \{ 1, 2, 3 \})\par > System.Console.WriteLine();\par 1\par 2\par 3", +@"[{""content"":""oreach (var o in new[] { 1, 2, 3 })\u000d\u000a"",""kind"":2},{""content"":""> "",""kind"":2},{""content"":""System.Console.WriteLine();\u000d\u000a"",""kind"":2},{""content"":""1\u000d\u000a2\u000d\u000a3"",""kind"":2}]"); } [Fact] @@ -662,7 +665,8 @@ public void CutWithinInput() VerifyClipboardData( @"each (var o in new[] { 1, 2, 3 }) System.Console.WriteLine()", - expectedRtf: null); + expectedRtf: null, + expectedRepl: null); } [Fact] @@ -684,7 +688,7 @@ public void CutInputAndOutput() Window.Operations.SelectAll(); Window.Operations.SelectAll(); Window.Operations.Cut(); - VerifyClipboardData(null); + VerifyClipboardData(null, null, null); } /// @@ -701,15 +705,15 @@ public void CopyNoSelection() @" 1 2 "); - CopyNoSelectionAndVerify(0, 7, "s +\r\n", @"> s +\par "); - CopyNoSelectionAndVerify(7, 11, "\r\n", @"> \par "); - CopyNoSelectionAndVerify(11, 17, " t\r\n", @"> t\par "); - CopyNoSelectionAndVerify(17, 21, " 1\r\n", @" 1\par "); - CopyNoSelectionAndVerify(21, 23, "\r\n", @"\par "); - CopyNoSelectionAndVerify(23, 28, "2 ", "2 > "); + CopyNoSelectionAndVerify(0, 7, "> s +\r\n", @"> s +\par ", @"[{""content"":""> "",""kind"":2},{""content"":""s +\u000d\u000a"",""kind"":2}]"); + CopyNoSelectionAndVerify(7, 11, "> \r\n", @"> \par ", @"[{""content"":""> "",""kind"":2},{""content"":""\u000d\u000a"",""kind"":2}]"); + CopyNoSelectionAndVerify(11, 17, "> t\r\n", @"> t\par ", @"[{""content"":""> "",""kind"":2},{""content"":"" t\u000d\u000a"",""kind"":2}]"); + CopyNoSelectionAndVerify(17, 21, " 1\r\n", @" 1\par ", @"[{""content"":"" 1\u000d\u000a"",""kind"":2}]"); + CopyNoSelectionAndVerify(21, 23, "\r\n", @"\par ", @"[{""content"":""\u000d\u000a"",""kind"":2}]"); + CopyNoSelectionAndVerify(23, 28, "2 > ", "2 > ", @"[{""content"":""2 "",""kind"":2},{""content"":""> "",""kind"":2}]"); } - private void CopyNoSelectionAndVerify(int start, int end, string expectedText, string expectedRtf) + private void CopyNoSelectionAndVerify(int start, int end, string expectedText, string expectedRtf, string expectedRepl) { var caret = Window.TextView.Caret; var snapshot = Window.TextView.TextBuffer.CurrentSnapshot; @@ -718,7 +722,81 @@ private void CopyNoSelectionAndVerify(int start, int end, string expectedText, s Clipboard.Clear(); caret.MoveTo(new SnapshotPoint(snapshot, i)); Window.Operations.Copy(); - VerifyClipboardData(expectedText, expectedRtf); + VerifyClipboardData(expectedText, expectedRtf, expectedRepl); + } + } + + [Fact] + public void Paste() + { + var blocks = new[] + { + new BufferBlock(ReplSpanKind.Output, "a\r\nbc"), + new BufferBlock(ReplSpanKind.Prompt, "> "), + new BufferBlock(ReplSpanKind.Prompt, "< "), + new BufferBlock(ReplSpanKind.Input, "12"), + new BufferBlock(ReplSpanKind.StandardInput, "3"), + new BufferBlock((ReplSpanKind)10, "xyz") + }; + + // Paste from text clipboard format. + CopyToClipboard(blocks, includeRepl: false); + Window.Operations.Paste(); + var text = Window.TextView.TextBuffer.CurrentSnapshot.GetText(); + Assert.Equal("> a\r\n> bc> < 123xyz", text); + + Window.Operations.ClearView(); + text = Window.TextView.TextBuffer.CurrentSnapshot.GetText(); + Assert.Equal("> ", text); + + // Paste from custom clipboard format. + CopyToClipboard(blocks, includeRepl: true); + Window.Operations.Paste(); + text = Window.TextView.TextBuffer.CurrentSnapshot.GetText(); + Assert.Equal("> a\r\n> bc123", text); + } + + private static void CopyToClipboard(BufferBlock[] blocks, bool includeRepl) + { + Clipboard.Clear(); + var data = new DataObject(); + var builder = new StringBuilder(); + foreach (var block in blocks) + { + builder.Append(block.Content); + } + var text = builder.ToString(); + data.SetData(DataFormats.UnicodeText, text); + data.SetData(DataFormats.StringFormat, text); + if (includeRepl) + { + data.SetData(InteractiveWindow.ClipboardFormat, BufferBlock.Serialize(blocks)); + } + Clipboard.SetDataObject(data, false); + } + + [Fact] + public void JsonSerialization() + { + var expectedContent = new [] + { + new BufferBlock(ReplSpanKind.Prompt, "> "), + new BufferBlock(ReplSpanKind.Input, "Hello"), + new BufferBlock(ReplSpanKind.Prompt, ". "), + new BufferBlock(ReplSpanKind.StandardInput, "world"), + new BufferBlock(ReplSpanKind.Output, "Hello world"), + }; + var actualJson = BufferBlock.Serialize(expectedContent); + var expectedJson = @"[{""content"":""> "",""kind"":0},{""content"":""Hello"",""kind"":2},{""content"":"". "",""kind"":0},{""content"":""world"",""kind"":3},{""content"":""Hello world"",""kind"":1}]"; + Assert.Equal(expectedJson, actualJson); + var actualContent = BufferBlock.Deserialize(actualJson); + Assert.Equal(expectedContent.Length, actualContent.Length); + for (int i = 0; i < expectedContent.Length; i++) + { + var expectedBuffer = expectedContent[i]; + var actualBuffer = actualContent[i]; + Assert.Equal(expectedBuffer.Kind, actualBuffer.Kind); + Assert.Equal(expectedBuffer.Content, actualBuffer.Content); } } @@ -770,17 +848,13 @@ private void Submit(string submission, string output) } } - private static void VerifyClipboardData(string expectedText) - { - VerifyClipboardData(expectedText, expectedText); - } - - private static void VerifyClipboardData(string expectedText, string expectedRtf) + private static void VerifyClipboardData(string expectedText, string expectedRtf, string expectedRepl) { var data = Clipboard.GetDataObject(); Assert.Equal(expectedText, data.GetData(DataFormats.StringFormat)); Assert.Equal(expectedText, data.GetData(DataFormats.Text)); Assert.Equal(expectedText, data.GetData(DataFormats.UnicodeText)); + Assert.Equal(expectedRepl, (string)data.GetData(InteractiveWindow.ClipboardFormat)); var actualRtf = (string)data.GetData(DataFormats.Rtf); if (expectedRtf == null) { diff --git a/src/InteractiveWindow/EditorTest/TestInteractiveEngine.cs b/src/InteractiveWindow/EditorTest/TestInteractiveEngine.cs index 7c9e9873de879..835c7f0d2112d 100644 --- a/src/InteractiveWindow/EditorTest/TestInteractiveEngine.cs +++ b/src/InteractiveWindow/EditorTest/TestInteractiveEngine.cs @@ -57,7 +57,7 @@ public Task ExecuteCodeAsync(string text) public string FormatClipboard() { - return ""; + return null; } public void AbortExecution() From ca5c3efc679d9344c81f5d41a5245b47fcbdda74 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Wed, 16 Sep 2015 14:10:07 -0700 Subject: [PATCH 2/2] Use NuGetPackageResolver in InteractiveHost --- src/EditorFeatures/Test/project.lock.json | 41 +++++++++ .../Core/InteractiveEditorFeatures.csproj | 1 - .../Core/InteractiveHost.Service.cs | 7 +- .../Core}/NuGetPackageResolverImpl.cs | 85 +++++++++--------- .../Features/InteractiveFeatures.csproj | 1 + src/Interactive/Features/project.json | 1 + src/Interactive/Features/project.lock.json | 34 ++++++++ src/Interactive/Host/project.lock.json | 41 +++++++++ .../HostTest/NuGetPackageResolverTests.cs | 87 ++++++++++++++----- .../Core/Resolvers/NuGetPackageResolver.cs | 27 +++++- .../RuntimeMetadataReferenceResolver.cs | 37 ++++---- src/Scripting/Core/ScriptOptions.cs | 12 +-- .../RuntimeMetadataReferenceResolverTests.cs | 78 +++++++++++++++++ src/Scripting/CoreTest/ScriptingTest.csproj | 3 +- 14 files changed, 362 insertions(+), 93 deletions(-) rename src/Interactive/{EditorFeatures/Core/Extensibility/Interactive => Features/Interactive/Core}/NuGetPackageResolverImpl.cs (70%) create mode 100644 src/Scripting/CoreTest/RuntimeMetadataReferenceResolverTests.cs diff --git a/src/EditorFeatures/Test/project.lock.json b/src/EditorFeatures/Test/project.lock.json index 3244d100c3a78..64571b6a0dc0c 100644 --- a/src/EditorFeatures/Test/project.lock.json +++ b/src/EditorFeatures/Test/project.lock.json @@ -59,6 +59,14 @@ "lib/net40/Moq.dll": {} } }, + "Newtonsoft.Json/6.0.4": { + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, "System.AppContext/4.0.0": { "dependencies": { "System.Runtime": "[4.0.0, )" @@ -368,6 +376,14 @@ "lib/net40/Moq.dll": {} } }, + "Newtonsoft.Json/6.0.4": { + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, "System.AppContext/4.0.0": { "dependencies": { "System.Runtime": "[4.0.0, )" @@ -776,6 +792,31 @@ "package/services/metadata/core-properties/98e2d674c8ec4e5fbda07a9e01280647.psmdcp" ] }, + "Newtonsoft.Json/6.0.4": { + "sha512": "FyQLmEpjsCrEP+znauLDGAi+h6i9YnaMkITlfIoiM4RYyX3nki306bTHsr/0okiIvIc7BJhQTbOAIZVocccFUw==", + "type": "Package", + "files": [ + "[Content_Types].xml", + "_rels/.rels", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/netcore45/Newtonsoft.Json.dll", + "lib/netcore45/Newtonsoft.Json.xml", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.xml", + "Newtonsoft.Json.nuspec", + "package/services/metadata/core-properties/87a0a4e28d50417ea282e20f81bc6477.psmdcp", + "tools/install.ps1" + ] + }, "System.AppContext/4.0.0": { "sha512": "gUoYgAWDC3+xhKeU5KSLbYDhTdBYk9GssrMSCcWUADzOglW+s0AmwVhOUGt2tL5xUl7ZXoYTPdA88zCgKrlG0A==", "type": "Package", diff --git a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj index 5d4c9dac773f5..886e1773a9ded 100644 --- a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj +++ b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj @@ -131,7 +131,6 @@ - diff --git a/src/Interactive/Features/Interactive/Core/InteractiveHost.Service.cs b/src/Interactive/Features/Interactive/Core/InteractiveHost.Service.cs index 5533c77bdadcc..dc6879f0746fb 100644 --- a/src/Interactive/Features/Interactive/Core/InteractiveHost.Service.cs +++ b/src/Interactive/Features/Interactive/Core/InteractiveHost.Service.cs @@ -25,7 +25,6 @@ using Roslyn.Utilities; using RuntimeMetadataReferenceResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.Hosting.RuntimeMetadataReferenceResolver; -using NuGetPackageResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.Hosting.NuGetPackageResolver; using GacFileResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.Hosting.GacFileResolver; namespace Microsoft.CodeAnalysis.Interactive @@ -166,9 +165,13 @@ public void Initialize(Type replServiceProviderType) private MetadataReferenceResolver CreateMetadataReferenceResolver(ImmutableArray searchPaths, string baseDirectory) { + var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var packagesDirectory = string.IsNullOrEmpty(userProfilePath) ? + null : + Path.Combine(userProfilePath, Path.Combine(".nuget", "packages")); return new RuntimeMetadataReferenceResolver( new RelativePathResolver(searchPaths, baseDirectory), - null, // TODO + string.IsNullOrEmpty(packagesDirectory) ? null : new NuGetPackageResolverImpl(packagesDirectory), new GacFileResolver( architectures: GacFileResolver.Default.Architectures, // TODO (tomat) preferredCulture: CultureInfo.CurrentCulture), // TODO (tomat) diff --git a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs b/src/Interactive/Features/Interactive/Core/NuGetPackageResolverImpl.cs similarity index 70% rename from src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs rename to src/Interactive/Features/Interactive/Core/NuGetPackageResolverImpl.cs index c409a55f7f66b..c87c1d5669acc 100644 --- a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs +++ b/src/Interactive/Features/Interactive/Core/NuGetPackageResolverImpl.cs @@ -10,13 +10,23 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using NuGetPackageResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.Hosting.NuGetPackageResolver; -namespace Microsoft.CodeAnalysis.Editor.Interactive +namespace Microsoft.CodeAnalysis.Interactive { - internal sealed class NuGetPackageResolverImpl : WORKSPACES::Microsoft.CodeAnalysis.Scripting.Hosting.NuGetPackageResolver + internal sealed class NuGetPackageResolverImpl : NuGetPackageResolver { private const string ProjectJsonFramework = "net46"; private const string ProjectLockJsonFramework = ".NETFramework,Version=v4.6"; + private const string EmptyNuGetConfig = +@" + + + + + + +"; private readonly string _packagesDirectory; private readonly Action _restore; @@ -28,35 +38,39 @@ internal NuGetPackageResolverImpl(string packagesDirectory, Action ResolveNuGetPackage(string reference) + internal new static bool TryParsePackageReference(string reference, out string name, out string version) { - string packageName; - string packageVersion; - if (!ParsePackageReference(reference, out packageName, out packageVersion)) - { - return default(ImmutableArray); - } + return NuGetPackageResolver.TryParsePackageReference(reference, out name, out version); + } + internal override ImmutableArray ResolveNuGetPackage(string packageName, string packageVersion) + { try { - var tempPath = PathUtilities.CombineAbsoluteAndRelativePaths(Path.GetTempPath(), Guid.NewGuid().ToString("D")); + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D")); var tempDir = Directory.CreateDirectory(tempPath); try { // Create project.json. - var projectJson = PathUtilities.CombineAbsoluteAndRelativePaths(tempPath, "project.json"); - using (var stream = File.OpenWrite(projectJson)) + var projectJsonPath = Path.Combine(tempPath, "project.json"); + using (var stream = File.OpenWrite(projectJsonPath)) using (var writer = new StreamWriter(stream)) { WriteProjectJson(writer, packageName, packageVersion); } - // Run "nuget.exe restore project.json" to generate project.lock.json. - NuGetRestore(projectJson); + // Create nuget.config with no package sources so restore + // uses the local cache only, no downloading. + var configPath = Path.Combine(tempPath, "nuget.config"); + File.WriteAllText(configPath, EmptyNuGetConfig); + + // Run "nuget.exe restore project.json -configfile nuget.config" + // to generate project.lock.json. + NuGetRestore(projectJsonPath, configPath); // Read the references from project.lock.json. - var projectLockJson = PathUtilities.CombineAbsoluteAndRelativePaths(tempPath, "project.lock.json"); - using (var stream = File.OpenRead(projectLockJson)) + var projectLockJsonPath = Path.Combine(tempPath, "project.lock.json"); + using (var stream = File.OpenRead(projectLockJsonPath)) using (var reader = new StreamReader(stream)) { return ReadProjectLockJson(_packagesDirectory, reader); @@ -76,25 +90,6 @@ internal override ImmutableArray ResolveNuGetPackage(string reference) return default(ImmutableArray); } - /// - /// Syntax is "id/version", matching references in project.lock.json. - /// - internal static bool ParsePackageReference(string reference, out string name, out string version) - { - var parts = reference.Split('/'); - if ((parts.Length == 2) && - (parts[0].Length > 0) && - (parts[1].Length > 0)) - { - name = parts[0]; - version = parts[1]; - return true; - } - name = null; - version = null; - return false; - } - /// /// Generate a project.json file with the packages as "dependencies". /// @@ -139,7 +134,7 @@ internal static ImmutableArray ReadProjectLockJson(string packagesDirect { foreach (var package in (JObject)target.Value) { - var packageRoot = PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, package.Key); + var packageRoot = Path.Combine(packagesDirectory, package.Key); var runtime = (JObject)GetPropertyValue((JObject)package.Value, "runtime"); if (runtime == null) { @@ -147,8 +142,14 @@ internal static ImmutableArray ReadProjectLockJson(string packagesDirect } foreach (var item in runtime) { - var path = PathUtilities.CombinePossiblyRelativeAndRelativePaths(packageRoot, item.Key); - builder.Add(path); + var relativePath = item.Key; + // Ignore placeholder "_._" files. + var name = Path.GetFileName(relativePath); + if (string.Equals(name, "_._", StringComparison.InvariantCulture)) + { + continue; + } + builder.Add(Path.Combine(packageRoot, relativePath)); } } break; @@ -164,17 +165,17 @@ private static JToken GetPropertyValue(JObject obj, string propertyName) return value; } - private void NuGetRestore(string projectJsonPath) + private void NuGetRestore(string projectJsonPath, string configPath) { // Load nuget.exe from same directory as current assembly. - var nugetExePath = PathUtilities.CombineAbsoluteAndRelativePaths( - PathUtilities.GetDirectoryName( + var nugetExePath = Path.Combine( + Path.GetDirectoryName( CorLightup.Desktop.GetAssemblyLocation(typeof(NuGetPackageResolverImpl).GetTypeInfo().Assembly)), "nuget.exe"); var startInfo = new ProcessStartInfo() { FileName = nugetExePath, - Arguments = $"restore \"{projectJsonPath}\" -PackagesDirectory \"{_packagesDirectory}\"", + Arguments = $"restore \"{projectJsonPath}\" -ConfigFile \"{configPath}\" -PackagesDirectory \"{_packagesDirectory}\"", CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/Interactive/Features/InteractiveFeatures.csproj b/src/Interactive/Features/InteractiveFeatures.csproj index e420f546b7ef8..df2553be0b54a 100644 --- a/src/Interactive/Features/InteractiveFeatures.csproj +++ b/src/Interactive/Features/InteractiveFeatures.csproj @@ -94,6 +94,7 @@ + diff --git a/src/Interactive/Features/project.json b/src/Interactive/Features/project.json index 18f2a512a54d8..66e51b9634f1a 100644 --- a/src/Interactive/Features/project.json +++ b/src/Interactive/Features/project.json @@ -1,6 +1,7 @@ { "dependencies": { "Microsoft.Composition": "1.0.27", + "Newtonsoft.Json": "6.0.4", "System.Collections": "4.0.10", "System.Diagnostics.Debug": "4.0.10", "System.Globalization": "4.0.10", diff --git a/src/Interactive/Features/project.lock.json b/src/Interactive/Features/project.lock.json index 5d3952aa3c4d6..ed0b7d4bc0333 100644 --- a/src/Interactive/Features/project.lock.json +++ b/src/Interactive/Features/project.lock.json @@ -27,6 +27,14 @@ "lib/net45/_._": {} } }, + "Newtonsoft.Json/6.0.4": { + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, "System.AppContext/4.0.0": { "dependencies": { "System.Runtime": "[4.0.0, )" @@ -364,6 +372,31 @@ "runtimes/aot/lib/netcore50/System.Xml.Serialization.dll" ] }, + "Newtonsoft.Json/6.0.4": { + "sha512": "FyQLmEpjsCrEP+znauLDGAi+h6i9YnaMkITlfIoiM4RYyX3nki306bTHsr/0okiIvIc7BJhQTbOAIZVocccFUw==", + "type": "Package", + "files": [ + "[Content_Types].xml", + "_rels/.rels", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/netcore45/Newtonsoft.Json.dll", + "lib/netcore45/Newtonsoft.Json.xml", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.xml", + "Newtonsoft.Json.nuspec", + "package/services/metadata/core-properties/87a0a4e28d50417ea282e20f81bc6477.psmdcp", + "tools/install.ps1" + ] + }, "System.AppContext/4.0.0": { "sha512": "gUoYgAWDC3+xhKeU5KSLbYDhTdBYk9GssrMSCcWUADzOglW+s0AmwVhOUGt2tL5xUl7ZXoYTPdA88zCgKrlG0A==", "type": "Package", @@ -1151,6 +1184,7 @@ "projectFileDependencyGroups": { "": [ "Microsoft.Composition >= 1.0.27", + "Newtonsoft.Json >= 6.0.4", "System.Collections >= 4.0.10", "System.Diagnostics.Debug >= 4.0.10", "System.Globalization >= 4.0.10", diff --git a/src/Interactive/Host/project.lock.json b/src/Interactive/Host/project.lock.json index 92189a09eca0c..ba6830a3ebebb 100644 --- a/src/Interactive/Host/project.lock.json +++ b/src/Interactive/Host/project.lock.json @@ -27,6 +27,14 @@ "lib/net45/_._": {} } }, + "Newtonsoft.Json/6.0.4": { + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, "System.AppContext/4.0.0": { "dependencies": { "System.Runtime": "[4.0.0, )" @@ -285,6 +293,14 @@ "lib/net45/_._": {} } }, + "Newtonsoft.Json/6.0.4": { + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, "System.AppContext/4.0.0": { "dependencies": { "System.Runtime": "[4.0.0, )" @@ -622,6 +638,31 @@ "runtimes/aot/lib/netcore50/System.Xml.Serialization.dll" ] }, + "Newtonsoft.Json/6.0.4": { + "sha512": "FyQLmEpjsCrEP+znauLDGAi+h6i9YnaMkITlfIoiM4RYyX3nki306bTHsr/0okiIvIc7BJhQTbOAIZVocccFUw==", + "type": "Package", + "files": [ + "[Content_Types].xml", + "_rels/.rels", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/netcore45/Newtonsoft.Json.dll", + "lib/netcore45/Newtonsoft.Json.xml", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.xml", + "Newtonsoft.Json.nuspec", + "package/services/metadata/core-properties/87a0a4e28d50417ea282e20f81bc6477.psmdcp", + "tools/install.ps1" + ] + }, "System.AppContext/4.0.0": { "sha512": "gUoYgAWDC3+xhKeU5KSLbYDhTdBYk9GssrMSCcWUADzOglW+s0AmwVhOUGt2tL5xUl7ZXoYTPdA88zCgKrlG0A==", "type": "Package", diff --git a/src/Interactive/HostTest/NuGetPackageResolverTests.cs b/src/Interactive/HostTest/NuGetPackageResolverTests.cs index 4277258666b1e..feca034d1debc 100644 --- a/src/Interactive/HostTest/NuGetPackageResolverTests.cs +++ b/src/Interactive/HostTest/NuGetPackageResolverTests.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.CodeAnalysis.Editor.Interactive; +using Microsoft.CodeAnalysis.Interactive; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Roslyn.Utilities; @@ -13,6 +13,9 @@ namespace Microsoft.CodeAnalysis.UnitTests.Interactive { public class NuGetPackageResolverTests : TestBase { + /// + /// Valid reference. + /// [ConditionalFact(typeof(WindowsOnly))] public void ResolveReference() { @@ -25,6 +28,15 @@ public void ResolveReference() ""net46"": {} } }"; + var expectedConfig = +@" + + + + + + +"; var actualProjectLockJson = @"{ ""locked"": false, @@ -48,6 +60,11 @@ public void ResolveReference() ""System.Runtime"": """" }, }, + ""System.Runtime/4.0.0"": { + ""runtime"": { + ""ref/dotnet/_._"": {} + } + }, ""System.IO/4.0.10"": { ""dependencies"": {}, ""runtime"": { @@ -65,23 +82,32 @@ public void ResolveReference() packagesDirectory, startInfo => { + // Verify arguments. var arguments = startInfo.Arguments.Split('"'); - Assert.Equal(5, arguments.Length); + Assert.Equal(7, arguments.Length); Assert.Equal("restore ", arguments[0]); Assert.Equal("project.json", PathUtilities.GetFileName(arguments[1])); - Assert.Equal(" -PackagesDirectory ", arguments[2]); - Assert.Equal(packagesDirectory, arguments[3]); - Assert.Equal("", arguments[4]); + Assert.Equal(" -ConfigFile ", arguments[2]); + Assert.Equal("nuget.config", PathUtilities.GetFileName(arguments[3])); + Assert.Equal(" -PackagesDirectory ", arguments[4]); + Assert.Equal(packagesDirectory, arguments[5]); + Assert.Equal("", arguments[6]); + // Verify project.json contents. var projectJsonPath = arguments[1]; var actualProjectJson = File.ReadAllText(projectJsonPath); Assert.Equal(expectedProjectJson, actualProjectJson); + // Verify config file contents. + var configPath = arguments[3]; + var actualConfig = File.ReadAllText(configPath); + Assert.Equal(expectedConfig, actualConfig); + // Generate project.lock.json. var projectLockJsonPath = PathUtilities.CombineAbsoluteAndRelativePaths(PathUtilities.GetDirectoryName(projectJsonPath), "project.lock.json"); using (var writer = new StreamWriter(projectLockJsonPath)) { writer.Write(actualProjectLockJson); } }); - var actualPaths = resolver.ResolveNuGetPackage("A.B.C/1.2"); + var actualPaths = resolver.ResolveNuGetPackage("A.B.C", "1.2"); AssertEx.SetEqual(actualPaths, PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, PathUtilities.CombinePossiblyRelativeAndRelativePaths("System.Collections/4.0.10", "ref/dotnet/System.Collections.dll")), PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, PathUtilities.CombinePossiblyRelativeAndRelativePaths("System.IO/4.0.10", "ref/dotnet/System.Runtime.dll")), @@ -89,24 +115,34 @@ public void ResolveReference() } } + /// + /// Expected exception thrown during restore. + /// [ConditionalFact(typeof(WindowsOnly))] public void HandledException() { using (var directory = new DisposableDirectory(Temp)) { - var resolver = new NuGetPackageResolverImpl(directory.Path, startInfo => { throw new IOException(); }); - var actualPaths = resolver.ResolveNuGetPackage("A.B.C/1.2"); + bool restored = false; + var resolver = new NuGetPackageResolverImpl(directory.Path, startInfo => { restored = true; throw new IOException(); }); + var actualPaths = resolver.ResolveNuGetPackage("A.B.C", "1.2"); Assert.True(actualPaths.IsDefault); + Assert.True(restored); } } + /// + /// Unexpected exception thrown during restore. + /// [ConditionalFact(typeof(WindowsOnly))] public void UnhandledException() { using (var directory = new DisposableDirectory(Temp)) { - var resolver = new NuGetPackageResolverImpl(directory.Path, startInfo => { throw new InvalidOperationException(); }); - Assert.Throws(() => resolver.ResolveNuGetPackage("A.B.C/1.2")); + bool restored = false; + var resolver = new NuGetPackageResolverImpl(directory.Path, startInfo => { restored = true; throw new InvalidOperationException(); }); + Assert.Throws(() => resolver.ResolveNuGetPackage("A.B.C", "1.2")); + Assert.True(restored); } } @@ -114,24 +150,31 @@ public void UnhandledException() public void ParsePackageNameAndVersion() { ParseInvalidPackageReference("A"); - ParseInvalidPackageReference("A.B"); - ParseInvalidPackageReference("A/"); - ParseInvalidPackageReference("A//1.0"); - ParseInvalidPackageReference("/1.0.0"); - ParseInvalidPackageReference("A/B/2.0.0"); + ParseInvalidPackageReference("A/1"); + ParseInvalidPackageReference("nuget"); + ParseInvalidPackageReference("nuget:"); + ParseInvalidPackageReference("NUGET:"); + ParseInvalidPackageReference("nugetA/1"); + ParseInvalidPackageReference("nuget:A"); + ParseInvalidPackageReference("nuget:A.B"); + ParseInvalidPackageReference("nuget:A/"); + ParseInvalidPackageReference("nuget:A//1.0"); + ParseInvalidPackageReference("nuget:/1.0.0"); + ParseInvalidPackageReference("nuget:A/B/2.0.0"); - ParseValidPackageReference("A/1", "A", "1"); - ParseValidPackageReference("A.B/1.0.0", "A.B", "1.0.0"); - ParseValidPackageReference("A/B.C", "A", "B.C"); - ParseValidPackageReference(" /1", " ", "1"); - ParseValidPackageReference("A\t/\n1.0\r ", "A\t", "\n1.0\r "); + ParseValidPackageReference("nuget::nuget/1", ":nuget", "1"); + ParseValidPackageReference("nuget:A/1", "A", "1"); + ParseValidPackageReference("nuget:A.B/1.0.0", "A.B", "1.0.0"); + ParseValidPackageReference("nuget:A/B.C", "A", "B.C"); + ParseValidPackageReference("nuget: /1", " ", "1"); + ParseValidPackageReference("nuget:A\t/\n1.0\r ", "A\t", "\n1.0\r "); } private static void ParseValidPackageReference(string reference, string expectedName, string expectedVersion) { string name; string version; - Assert.True(NuGetPackageResolverImpl.ParsePackageReference(reference, out name, out version)); + Assert.True(NuGetPackageResolverImpl.TryParsePackageReference(reference, out name, out version)); Assert.Equal(expectedName, name); Assert.Equal(expectedVersion, version); } @@ -140,7 +183,7 @@ private static void ParseInvalidPackageReference(string reference) { string name; string version; - Assert.False(NuGetPackageResolverImpl.ParsePackageReference(reference, out name, out version)); + Assert.False(NuGetPackageResolverImpl.TryParsePackageReference(reference, out name, out version)); Assert.Null(name); Assert.Null(version); } diff --git a/src/Scripting/Core/Resolvers/NuGetPackageResolver.cs b/src/Scripting/Core/Resolvers/NuGetPackageResolver.cs index 94c2ebd45f4a5..1f33651255424 100644 --- a/src/Scripting/Core/Resolvers/NuGetPackageResolver.cs +++ b/src/Scripting/Core/Resolvers/NuGetPackageResolver.cs @@ -1,11 +1,36 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Immutable; namespace Microsoft.CodeAnalysis.Scripting.Hosting { internal abstract class NuGetPackageResolver { - internal abstract ImmutableArray ResolveNuGetPackage(string reference); + private const string ReferencePrefix = "nuget:"; + + /// + /// Syntax is "nuget:id/version". + /// + internal static bool TryParsePackageReference(string reference, out string name, out string version) + { + if (reference.StartsWith(ReferencePrefix, StringComparison.Ordinal)) + { + var parts = reference.Substring(ReferencePrefix.Length).Split('/'); + if ((parts.Length == 2) && + (parts[0].Length > 0) && + (parts[1].Length > 0)) + { + name = parts[0]; + version = parts[1]; + return true; + } + } + name = null; + version = null; + return false; + } + + internal abstract ImmutableArray ResolveNuGetPackage(string packageName, string packageVersion); } } diff --git a/src/Scripting/Core/Resolvers/RuntimeMetadataReferenceResolver.cs b/src/Scripting/Core/Resolvers/RuntimeMetadataReferenceResolver.cs index 47b3db50b9821..8a7c45c27da66 100644 --- a/src/Scripting/Core/Resolvers/RuntimeMetadataReferenceResolver.cs +++ b/src/Scripting/Core/Resolvers/RuntimeMetadataReferenceResolver.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Immutable; -using System.IO; +using System.Diagnostics; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Scripting.Hosting @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Scripting.Hosting /// internal sealed class RuntimeMetadataReferenceResolver : MetadataReferenceResolver, IEquatable { - public static readonly RuntimeMetadataReferenceResolver Default = new RuntimeMetadataReferenceResolver(); + public static readonly RuntimeMetadataReferenceResolver Default = new RuntimeMetadataReferenceResolver(ImmutableArray.Empty, baseDirectory: null); internal readonly RelativePathResolver PathResolver; internal readonly NuGetPackageResolver PackageResolver; @@ -23,8 +23,8 @@ internal sealed class RuntimeMetadataReferenceResolver : MetadataReferenceResolv private readonly Func _fileReferenceProvider; internal RuntimeMetadataReferenceResolver( - ImmutableArray searchPaths = default(ImmutableArray), - string baseDirectory = null) + ImmutableArray searchPaths, + string baseDirectory) : this(new RelativePathResolver(searchPaths.NullToEmpty(), baseDirectory), null, GacFileResolver.Default) @@ -46,27 +46,29 @@ internal RuntimeMetadataReferenceResolver( public override ImmutableArray ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties) { - if (PathResolver != null && PathUtilities.IsFilePath(reference)) + string packageName; + string packageVersion; + if (NuGetPackageResolver.TryParsePackageReference(reference, out packageName, out packageVersion)) { - var resolvedPath = PathResolver.ResolvePath(reference, baseFilePath); - if (resolvedPath == null) + if (PackageResolver != null) { - return ImmutableArray.Empty; + var paths = PackageResolver.ResolveNuGetPackage(packageName, packageVersion); + Debug.Assert(!paths.IsDefault); + return paths.SelectAsArray(path => _fileReferenceProvider(path, properties)); } - - return ImmutableArray.Create(_fileReferenceProvider(resolvedPath, properties)); } - - if (PackageResolver != null) + else if (PathUtilities.IsFilePath(reference)) { - var paths = PackageResolver.ResolveNuGetPackage(reference); - if (!paths.IsDefaultOrEmpty) + if (PathResolver != null) { - return paths.SelectAsArray(path => _fileReferenceProvider(path, properties)); + var resolvedPath = PathResolver.ResolvePath(reference, baseFilePath); + if (resolvedPath != null) + { + return ImmutableArray.Create(_fileReferenceProvider(resolvedPath, properties)); + } } } - - if (GacFileResolver != null) + else if (GacFileResolver != null) { var path = GacFileResolver.Resolve(reference); if (path != null) @@ -74,7 +76,6 @@ public override ImmutableArray ResolveReference(str return ImmutableArray.Create(_fileReferenceProvider(path, properties)); } } - return ImmutableArray.Empty; } diff --git a/src/Scripting/Core/ScriptOptions.cs b/src/Scripting/Core/ScriptOptions.cs index b7af60d7de230..88c4614fe76c1 100644 --- a/src/Scripting/Core/ScriptOptions.cs +++ b/src/Scripting/Core/ScriptOptions.cs @@ -340,7 +340,9 @@ private static ImmutableArray CheckImmutableArray(ImmutableArray items, private static ImmutableArray ToImmutableArrayChecked(IEnumerable items, string parameterName) where T : class { - return AddRangeAndFreeChecked(ArrayBuilder.GetInstance(), items, parameterName); + var builder = ArrayBuilder.GetInstance(); + AddRangeChecked(builder, items, parameterName); + return builder.ToImmutableAndFree(); } private static ImmutableArray ConcatChecked(ImmutableArray existing, IEnumerable items, string parameterName) @@ -348,10 +350,11 @@ private static ImmutableArray ConcatChecked(ImmutableArray existing, IE { var builder = ArrayBuilder.GetInstance(); builder.AddRange(existing); - return AddRangeAndFreeChecked(builder, items, parameterName); + AddRangeChecked(builder, items, parameterName); + return builder.ToImmutableAndFree(); } - private static ImmutableArray AddRangeAndFreeChecked(ArrayBuilder builder, IEnumerable items, string parameterName) + private static void AddRangeChecked(ArrayBuilder builder, IEnumerable items, string parameterName) where T : class { RequireNonNull(items, parameterName); @@ -360,14 +363,11 @@ private static ImmutableArray AddRangeAndFreeChecked(ArrayBuilder build { if (item == null) { - builder.Free(); throw new ArgumentNullException($"{parameterName}[{builder.Count}]"); } builder.Add(item); } - - return builder.ToImmutableAndFree(); } private static IEnumerable SelectChecked(IEnumerable items, string parameterName, Func selector) diff --git a/src/Scripting/CoreTest/RuntimeMetadataReferenceResolverTests.cs b/src/Scripting/CoreTest/RuntimeMetadataReferenceResolverTests.cs new file mode 100644 index 0000000000000..a6007fec5fa25 --- /dev/null +++ b/src/Scripting/CoreTest/RuntimeMetadataReferenceResolverTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Scripting.Hosting; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using System.Collections.Immutable; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.Interactive +{ + public class RuntimeMetadataReferenceResolverTests : TestBase + { + [Fact] + public void Resolve() + { + using (var directory = new DisposableDirectory(Temp)) + { + var assembly1 = directory.CreateFile("_1.dll"); + var assembly2 = directory.CreateFile("_2.dll"); + + // With NuGetPackageResolver. + var resolver = new RuntimeMetadataReferenceResolver( + new RelativePathResolver(ImmutableArray.Create(directory.Path), baseDirectory: directory.Path), + new PackageResolver(ImmutableDictionary>.Empty.Add("nuget:N/1.0", ImmutableArray.Create(assembly1.Path, assembly2.Path))), + gacFileResolver: null); + // Recognized NuGet reference. + var actualReferences = resolver.ResolveReference("nuget:N/1.0", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + AssertEx.SetEqual(actualReferences.SelectAsArray(r => r.FilePath), assembly1.Path, assembly2.Path); + // Unrecognized NuGet reference. + actualReferences = resolver.ResolveReference("nuget:N/2.0", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + Assert.True(actualReferences.IsEmpty); + // Recognized file path. + actualReferences = resolver.ResolveReference("_2.dll", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + AssertEx.SetEqual(actualReferences.SelectAsArray(r => r.FilePath), assembly2.Path); + // Unrecognized file path. + actualReferences = resolver.ResolveReference("_3.dll", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + Assert.True(actualReferences.IsEmpty); + + // Without NuGetPackageResolver. + resolver = new RuntimeMetadataReferenceResolver( + new RelativePathResolver(ImmutableArray.Create(directory.Path), baseDirectory: directory.Path), + packageResolver: null, + gacFileResolver: null); + // Unrecognized NuGet reference. + actualReferences = resolver.ResolveReference("nuget:N/1.0", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + Assert.True(actualReferences.IsEmpty); + // Recognized file path. + actualReferences = resolver.ResolveReference("_2.dll", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + AssertEx.SetEqual(actualReferences.SelectAsArray(r => r.FilePath), assembly2.Path); + // Unrecognized file path. + actualReferences = resolver.ResolveReference("_3.dll", baseFilePath: null, properties: MetadataReferenceProperties.Assembly); + Assert.True(actualReferences.IsEmpty); + } + } + + private sealed class PackageResolver : NuGetPackageResolver + { + private const string Prefix = "nuget:"; + private readonly IImmutableDictionary> _map; + + internal PackageResolver(IImmutableDictionary> map) + { + _map = map; + } + + internal override ImmutableArray ResolveNuGetPackage(string packageName, string packageVersion) + { + var reference = $"{Prefix}{packageName}/{packageVersion}"; + ImmutableArray paths; + if (_map.TryGetValue(reference, out paths)) + { + return paths; + } + return ImmutableArray.Empty; + } + } + } +} diff --git a/src/Scripting/CoreTest/ScriptingTest.csproj b/src/Scripting/CoreTest/ScriptingTest.csproj index 548f2f1569ac4..31547e8ffd217 100644 --- a/src/Scripting/CoreTest/ScriptingTest.csproj +++ b/src/Scripting/CoreTest/ScriptingTest.csproj @@ -59,6 +59,7 @@ + @@ -79,4 +80,4 @@ - + \ No newline at end of file