diff --git a/.editorconfig b/.editorconfig index d88d63c0..0d955f48 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,6 +40,11 @@ indent_size = 2 indent_style = space indent_size = 4 +# VS Code config files +[*.vscode/*.json] +indent_style = tab +indent_size = 2 + # Dotnet code style settings: [*.{cs,vb}] @@ -154,31 +159,31 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cffe41e..38565931 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,10 +29,6 @@ jobs: dotnet-version: | 8.0.x - - name: Find MSBuild - if: startsWith(matrix.os, 'windows') - uses: microsoft/setup-msbuild@v2 - - uses: actions/cache@v4 with: path: ${{ env.NUGET_PACKAGES }} @@ -40,22 +36,12 @@ jobs: restore-keys: | ${{ runner.os }}-nuget- - - name: Restore (dotnet) - if: startsWith(matrix.os, 'macos') + - name: Restore run: dotnet restore -p:Configuration=${{ matrix.config }} - - name: Build (dotnet) - if: startsWith(matrix.os, 'macos') + - name: Build run: dotnet build --no-restore -c ${{ matrix.config }} -p:CreatePackage=true - - name: Restore (MSBuild) - if: startsWith(matrix.os, 'windows') - run: msbuild -t:Restore -p:Configuration=${{ matrix.config }} - - - name: Build (MSBuild) - if: startsWith(matrix.os, 'windows') - run: msbuild MonoDevelop.MSBuildEditor.sln -p:Configuration=${{ matrix.config }} - - name: Test run: dotnet test --no-build -c ${{ matrix.config }} @@ -65,9 +51,9 @@ jobs: name: MSBuild Editor Extension Package (VSWin) path: MonoDevelop.MSBuild.Editor.VisualStudio/bin/**/*.vsix - - uses: actions/upload-artifact@v4 - if: startsWith(matrix.os, 'macos') - with: - name: MSBuild Editor Extension Package (VSMac) - path: MonoDevelop.MSBuildEditor/bin/**/*.mpack - if-no-files-found: error \ No newline at end of file +# - uses: actions/upload-artifact@v4 +# if: startsWith(matrix.os, 'macos') +# with: +# name: MSBuild Editor Extension Package (VSMac) +# path: MonoDevelop.MSBuildEditor/bin/**/*.mpack +# if-no-files-found: error \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9d8c0749..a8627889 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ packages .vs *.user *.binlog +out +node_modules +.vscode-test +dist \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d98c942d..dc7bdd16 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ -[submodule "external/ProjFileTools"] - path = external/ProjFileTools - url = https://github.com/mhutch/ProjFileTools.git [submodule "MonoDevelop.Xml"] path = MonoDevelop.Xml url = https://github.com/mhutch/MonoDevelop.Xml.git [submodule "external/NuGet.Client"] path = external/NuGet.Client url = https://github.com/NuGet/NuGet.Client.git +[submodule "external/roslyn"] + path = external/roslyn + url = https://github.com/dotnet/roslyn.git diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..74344417 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "amodio.tsl-problem-matcher", + "ms-vscode.extension-test-runner", + "ms-dotnettools.csharp", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "connor4312.esbuild-problem-matchers", + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..52bf01c8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run VS Code Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/msbuild-editor-vscode", + ], + "env": { + "MSBUILD_LANGUAGE_SERVER_PATH": "${workspaceFolder}/artifacts/bin/MSBuildLanguageServer/debug/MSBuildLanguageServer.dll", + }, + "outFiles": [ + "${workspaceFolder}/msbuild-editor-vscode/dist/**/*.js" + ], + "sourceMaps": true, + "preLaunchTask": "Build VS Code Extension in Background", + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 50178837..e17c2bb2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,12 @@ "/*.buildschema.json" ], "url": "./MonoDevelop.MSBuild/Schemas/buildschema.json" + }, + { + "fileMatch": [ + "/*.tmLanguage.json" + ], + "url": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json" } ], "cSpell.words": [ @@ -19,16 +25,29 @@ "fxcop", "Hotpatchable", "LCID", + "msbuild", "mscorlib", "Multitargeting", "netstandard", "overridable", "Precompiled", + "refactorings", "resx", "ruleset", "Struct", "Toolset", "unescaping", "VSINSTALLDIR" - ] + ], + "files.exclude": { + "msbuild-editor-vscode/out": false, + "msbuild-editor-vscode/dist": false + }, + "search.exclude": { + "msbuild-editor-vscode/out": true, + "msbuild-editor-vscode/dist": true + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "debug.allowBreakpointsEverywhere": true } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..78b0f1da --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,86 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build VS Code Extension in Background", + "dependsOn": [ + "npm: watch:tsc", + "npm: watch:esbuild" + ], + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "npm: watch:esbuild", + "type": "npm", + "script": "watch:esbuild", + "path": "msbuild-editor-vscode", + "group": "build", + "problemMatcher": { + "base": "$esbuild-watch", + "fileLocation": [ + "relative", + "${workspaceFolder}/msbuild-editor-vscode" + ] + }, + "isBackground": true, + "presentation": { + "group": "watch", + "reveal": "never" + } + }, + { + "label": "npm: watch:tsc", + "type": "npm", + "script": "watch:tsc", + "path": "msbuild-editor-vscode", + "group": "build", + "problemMatcher": { + "base": "$tsc-watch", + "fileLocation": [ + "relative", + "${workspaceFolder}/msbuild-editor-vscode" + ] + }, + "isBackground": true, + "presentation": { + "group": "watch", + "reveal": "never" + } + }, + { + "label": "npm: watch-tests", + "type": "npm", + "script": "watch-tests", + "path": "msbuild-editor-vscode", + "problemMatcher": { + "base": "$tsc-watch", + "fileLocation": [ + "relative", + "${workspaceFolder}/msbuild-editor-vscode" + ] + }, + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": "build" + }, + { + "label": "Build VS Code Tests in Background", + "dependsOn": [ + "npm: watch", + "npm: watch-tests" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 2ad97360..f5c6b347 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,5 +7,6 @@ embedded $(MSBuildProjectName) true + true \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 691ef465..c26c4c9b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,47 +1,66 @@ - - 17.5.279 + 4.12.0-1.final - + + + - - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + diff --git a/MSBuildLanguageServer.Tests/.editorconfig b/MSBuildLanguageServer.Tests/.editorconfig new file mode 100644 index 00000000..1fe8c7a3 --- /dev/null +++ b/MSBuildLanguageServer.Tests/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: http://EditorConfig.org + +[*.cs] + +# revert settings to match roslyn style better +indent_style = space +trim_trailing_whitespace = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_keywords_in_control_flow_statements = false + +# Newline settings +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false + +# VS threading analyzer triggers on imported roslyn code +dotnet_diagnostic.VSTHRD002.severity = none +dotnet_diagnostic.VSTHRD003.severity = none +dotnet_diagnostic.VSTHRD103.severity = none +dotnet_diagnostic.VSTHRD110.severity = none \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Completion/CompletionTests.cs b/MSBuildLanguageServer.Tests/Completion/CompletionTests.cs new file mode 100644 index 00000000..b26ff94b --- /dev/null +++ b/MSBuildLanguageServer.Tests/Completion/CompletionTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer; + +using MonoDevelop.Xml.Tests.Utils; + +using Roslyn.Test.Utilities; + +using Xunit.Abstractions; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.LanguageServer.Tests.Completion; + +public class CompletionTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + [Fact] + public async Task ResolveCompletionItem() + { + (string documentText, var caret) = TextWithMarkers.ExtractSingleLineColPosition (@"", '|'); + + var capabilities = new LSP.ClientCapabilities { + TextDocument = new LSP.TextDocumentClientCapabilities { + Completion = new LSP.CompletionSetting () + } + }; + + InitializationOptions initializationOptions = new() { + ClientCapabilities = capabilities, + ClientMessageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(excludeVSExtensionConverters: true) + }; + + await using var testLspServer = await CreateTestLspServerAsync(documentText, false, initializationOptions); + + var documentUri = new Uri("file://foo.csproj"); + + await testLspServer.OpenDocument(documentUri, documentText); + + var completionList = await testLspServer.GetCompletionList(documentUri, caret.AsLspPosition ()); + + var firstItem = completionList.Items[0]; + Assert.Equal("Project", firstItem.Label); + Assert.Null(firstItem.Documentation); + + var resolved = await testLspServer.ResolveCompletionItem(firstItem); + Assert.NotNull(resolved.Documentation); + } +} diff --git a/MSBuildLanguageServer.Tests/Completion/MSBuildCompletionTests.cs b/MSBuildLanguageServer.Tests/Completion/MSBuildCompletionTests.cs new file mode 100644 index 00000000..79f843b9 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Completion/MSBuildCompletionTests.cs @@ -0,0 +1,544 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Test.Utilities; + +using MonoDevelop.MSBuild.LanguageServer.Tests; +using MonoDevelop.Xml.Tests.Utils; + +using Roslyn.Test.Utilities; + +using Xunit.Abstractions; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Tests.Editor.Completion; + +public class MSBuildCompletionTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + [Fact] + public async Task ProjectElementCompletion() + { + var result = await this.GetCompletionContext("$"); + Assert.NotNull(result); + result.AssertNonEmpty(); + + result.AssertContains(""); + + result.AssertNonEmpty(); + + result.AssertContains("$"); + + result.AssertNonEmpty(); + + result.AssertContains("<$"); + + result.AssertItemCount(12); + + result.AssertContains("<$"); + + result.AssertContains("<$"); + + result.AssertContains("a<$"); + + result.AssertItemCount(5); + + result.AssertContains(" + + + + +Foo;Bar + + +Foo;Bar + + + + + + + + + + + + +$([MSBuild]::^", caretMarker: '^'); + + // check a few different expected values are in the list + result.AssertContains("GetDirectoryNameOfFileAbove"); + result.AssertContains("Add"); + result.AssertContains("GetTargetPlatformVersion"); + } + + [Fact] + public async Task StaticPropertyFunctionCompletion() + { + var result = await this.GetCompletionContext(@" + + +$([System.String]::^", caretMarker: '^'); + + result.AssertNonEmpty(); + + result.AssertContains("new"); + result.AssertContains("Join"); + result.AssertDoesNotContain("ToLower"); + } + + [Fact] + public async Task PropertyStringFunctionCompletion() + { + var result = await this.GetCompletionContext(@" + + +$(Foo.^", caretMarker: '^'); + + result.AssertNonEmpty(); + + //string functions + result.AssertContains("ToLower"); + //properties can be accessed with the getter method + result.AssertContains("get_Length"); + //.net properties are allowed for properties + result.AssertContains("Length"); + //indexers should be filtered out + result.AssertDoesNotContain("this[]"); + // ctors should be filtered out, cannot call on existing instance + result.AssertDoesNotContain("new"); + } + + [Fact] + public async Task PropertyFunctionArrayPropertyCompletion() + { + var result = await this.GetCompletionContext(@" + + +$([System.IO.Directory]::GetDirectories('.').^", caretMarker: '^'); + + result.AssertNonEmpty(); + result.AssertContains("Length"); + } + + [Fact] + public async Task PropertyFunctionArrayIndexerCompletion() + { + var result = await this.GetCompletionContext(@" + + +$([System.IO.Directory]::GetDirectories('.')[0].^", caretMarker: '^'); + + result.AssertNonEmpty(); + result.AssertContains("get_Chars"); + } + + [Fact] + public async Task ItemFunctionCompletion() + { + var result = await this.GetCompletionContext(@" + + +@(Foo->^", caretMarker: '^'); + + result.AssertNonEmpty(); + + //intrinsic functions + result.AssertContains("DistinctWithCase"); + result.AssertContains("Metadata"); + //string functions + result.AssertContains("ToLower"); + //properties can be accessed with the getter method + result.AssertContains("get_Length"); + //.net properties are not allowed for items + result.AssertDoesNotContain("Length"); + //indexers should be filtered out + result.AssertDoesNotContain("this[]"); + } + + [Fact] + public async Task PropertyFunctionClassNames() + { + var result = await this.GetCompletionContext(@" + + +$([^", caretMarker: '^'); + + result.AssertNonEmpty(); + result.AssertContains("MSBuild"); + result.AssertContains("System.String"); + } + + [Fact] + public async Task PropertyFunctionChaining() + { + var result = await this.GetCompletionContext(@" + + +$([System.DateTime]::Now.^", caretMarker: '^'); + + result.AssertNonEmpty(); + result.AssertContains("AddDays"); + } + + [Fact] + public async Task IndexerChaining() + { + var result = await this.GetCompletionContext(@" + + +$(Foo[0].^", caretMarker: '^'); + + result.AssertNonEmpty(); + result.AssertContains("CompareTo"); + result.AssertDoesNotContain("Substring"); + } + + [Fact] + public async Task EagerAttributeTrigger() + { + var result = await this.GetCompletionContext(@"$", + filename: "EagerElementTrigger.csproj", + composition: EditorFeaturesLspComposition.AddParts(typeof(TestSchemaProvider)) + ); + + result.AssertNonEmpty(); + result.AssertContains("True"); + } + + // LSP doesn't support trigger on backspace, disable these for now + /* + [Fact] + public async Task TriggerOnBackspace() + { + var result = await this.GetCompletionContext( + @"$", + CompletionTriggerReason.Backspace, + filename: "EagerElementTrigger.csproj"); + + result.AssertNonEmpty(); + result.AssertContains("True"); + } + + [Fact] + public async Task NoTriggerOnBackspaceMidExpression() + { + var result = await this.GetCompletionContext( + @"true$", + CompletionTriggerReason.Backspace, + filename: "EagerElementTrigger.csproj"); + + Assert.Zero(result.ItemList.Count); + } + */ + + [Fact] + public async Task TriggerOnFirstNameChar() + { + var result = await this.GetCompletionContext( + @"f$", + filename: testDirectory.Combine("PathCompletion.csproj"), + composition: EditorFeaturesLspComposition.AddParts(typeof(TestMSBuildFileSystemExport)) + ); + + result.AssertNonEmpty(); + result.AssertContains("foo.txt"); + } + + [Fact] + public async Task PathCompletionDirectory() + { + var testDirectory = TestMSBuildFileSystem.Instance.AddTestDirectory(); + testDirectory.AddFiles("foo.txt", "bar.cs", "baz.cs"); + testDirectory.AddDirectory("foo").AddFiles("hello.cs", "bye.cs"); + + var result = await this.GetCompletionContext( + @"foo\$", + filename: testDirectory.Combine("PathCompletion.csproj"), + composition: EditorFeaturesLspComposition.AddParts(typeof(TestMSBuildFileSystemExport)) + ); + + result.AssertNonEmpty(); + result.AssertContains("hello.cs"); + } + + async Task GetCompletionContext( + string documentText, + LSP.CompletionTriggerKind? triggerKind = null, + char? triggerChar = null, + char caretMarker = '$', + string? filename = default, + TestComposition? composition = null, + CancellationToken cancellationToken = default) + { + (documentText, var caret) = TextWithMarkers.ExtractSingleLineColPosition(documentText, caretMarker); + var caretPos = new LSP.Position { Line = caret.Line, Character = caret.Column }; + + var capabilities = new LSP.ClientCapabilities { + TextDocument = new LSP.TextDocumentClientCapabilities { + Completion = new LSP.CompletionSetting() + } + }; + + InitializationOptions initializationOptions = new() { + ClientCapabilities = capabilities, + ClientMessageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(excludeVSExtensionConverters: true) + }; + + await using var testLspServer = await CreateTestLspServerAsync(documentText, false, initializationOptions, composition); + + Uri documentUri; + if (filename is not null) + { + documentUri = ProtocolConversions.CreateAbsoluteUri(Path.GetFullPath(filename)); + } + else + { + documentUri = new Uri("file://foo.csproj"); + } + + await testLspServer.OpenDocument(documentUri, documentText, cancellationToken); + + return await testLspServer.GetCompletionList(documentUri, caretPos, triggerKind, triggerChar, cancellationToken); + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Extensions/CompletionListExtensions.cs b/MSBuildLanguageServer.Tests/Extensions/CompletionListExtensions.cs new file mode 100644 index 00000000..4e527ff7 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Extensions/CompletionListExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.LanguageServer.Tests; + +static class CompletionListExtensions +{ + public static void AssertContains(this LSP.CompletionList list, string name) + { + var item = list.Items.FirstOrDefault(i => i.Label == name); + Assert.NotNull(item); // "Completion result is missing item '{0}'", name); + } + + public static void AssertNonEmpty([NotNull] this LSP.CompletionList? list) + { + Assert.NotNull(list); + Assert.NotEmpty(list.Items); + } + + public static void AssertItemCount([NotNull] this LSP.CompletionList? list, int expectedCount) + { + Assert.NotNull(list); + Assert.Equal(expectedCount, list.Items.Length); + } + + public static void AssertDoesNotContain(this LSP.CompletionList list, string name) + { + var item = list.Items.FirstOrDefault(i => i.Label == name); + Assert.Null(item); //, "Completion result has unexpected item '{0}'", name); + } +} diff --git a/MSBuildLanguageServer.Tests/Extensions/TestServerExtensions.cs b/MSBuildLanguageServer.Tests/Extensions/TestServerExtensions.cs new file mode 100644 index 00000000..ee4839ba --- /dev/null +++ b/MSBuildLanguageServer.Tests/Extensions/TestServerExtensions.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis.LanguageServer; +using MonoDevelop.Xml.Tests.Utils; +using LSP = Roslyn.LanguageServer.Protocol; + +using static Roslyn.Test.Utilities.AbstractLanguageServerProtocolTests; + +namespace MonoDevelop.MSBuild.LanguageServer.Tests; + +static class TestServerExtensions +{ + public static async Task OpenDocumentAndGetCompletionList(this TestLspServer testLspServer, string documentText, char caretMarker = '$', string? filename = default, CancellationToken cancellationToken = default) + { + (documentText, var caret) = TextWithMarkers.ExtractSingleLineColPosition(documentText, '$'); + + var documentUri = new Uri(filename ?? "file://foo.csproj"); + + await testLspServer.OpenDocument(documentUri, documentText, cancellationToken); + + return await testLspServer.GetCompletionList(documentUri, caret.AsLspPosition(), cancellationToken: cancellationToken); + } + + public static async Task OpenDocument(this TestLspServer testLspServer, Uri documentUri, string documentText, CancellationToken cancellationToken = default) + { + await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentDidOpenName, + new LSP.DidOpenTextDocumentParams { + TextDocument = new LSP.TextDocumentItem { + Text = documentText, + Uri = documentUri, + LanguageId = LanguageName.MSBuild, + Version = 0 + } + }, + cancellationToken); + } + + public static async Task GetCompletionList( + this TestLspServer testLspServer, + Uri documentUri, + LSP.Position caretPosition, + LSP.CompletionTriggerKind? triggerKind = null, + char? triggerChar = null, + CancellationToken cancellationToken = default) + { + var completionParams = new LSP.CompletionParams { + TextDocument = new LSP.TextDocumentIdentifier { Uri = documentUri }, + Position = caretPosition + }; + + if (triggerChar.HasValue || triggerKind.HasValue) + { + completionParams.Context = new LSP.CompletionContext { + TriggerCharacter = triggerChar?.ToString(), + TriggerKind = triggerKind ?? ( triggerChar.HasValue? LSP.CompletionTriggerKind.TriggerCharacter : LSP.CompletionTriggerKind.Invoked) + }; + } + + var result = await testLspServer.ExecuteRequestAsync>( + LSP.Methods.TextDocumentCompletionName, + completionParams, + cancellationToken); + + var completionList = Assert.IsType(result.Value); + return completionList; + } + + public static async Task ResolveCompletionItem(this TestLspServer testLspServer, LSP.CompletionItem item, CancellationToken cancellationToken = default) + { + var resolved = await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentCompletionResolveName, + item, + CancellationToken.None); + Assert.NotNull(resolved); + return resolved; + } + + public static LSP.Position AsLspPosition(this TextMarkerPosition position) => new LSP.Position { Line = position.Line, Character = position.Column }; +} diff --git a/MSBuildLanguageServer.Tests/HoverTests.cs b/MSBuildLanguageServer.Tests/HoverTests.cs new file mode 100644 index 00000000..397314c0 --- /dev/null +++ b/MSBuildLanguageServer.Tests/HoverTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer; + +using MonoDevelop.Xml.Tests.Utils; + +using Roslyn.Test.Utilities; + +using Xunit.Abstractions; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Tests; + +public class HoverTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + [Fact] + public async Task TestGetHoverAsync() + { + (string documentText, int caretPos) = TextWithMarkers.ExtractSinglePosition (@"", '|'); + + var capabilities = new LSP.ClientCapabilities { + TextDocument = new LSP.TextDocumentClientCapabilities { + Hover = new LSP.HoverSetting { + ContentFormat = [ LSP.MarkupKind.PlainText ] + } + } + }; + + await using var testLspServer = await CreateTestLspServerAsync(documentText, false, capabilities); + + var documentId = new LSP.TextDocumentIdentifier { Uri = new Uri("file://foo.csproj") }; + + await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentDidOpenName, + new LSP.DidOpenTextDocumentParams { + TextDocument = new LSP.TextDocumentItem { + Text = documentText, + Uri = documentId.Uri, + LanguageId = LanguageName.MSBuild, + Version = 0 + } + }, + CancellationToken.None); + + + var caret = new LSP.TextDocumentPositionParams { + TextDocument = documentId, + Position = new LSP.Position { Line = 0, Character = caretPos } + }; + + var result = await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentHoverName, + caret, + CancellationToken.None); + + Assert.NotNull(result); + + var markup = Assert.IsType(result.Contents.Value); + var markupValue = markup.Value.Replace("\r\n", "\n"); + + Assert.Equal("keyword Project\nAn MSBuild project.", markupValue); + } +} diff --git a/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.Edited.cs b/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.Edited.cs new file mode 100644 index 00000000..5eb76872 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.Edited.cs @@ -0,0 +1,20 @@ +// heavily edited methods from +// https://github.com/dotnet/roslyn/blob/1a4c3f429fe13a2e928c800cebbf93154447095a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Editor.UnitTests; + +namespace Roslyn.Test.Utilities +{ + partial class AbstractLanguageServerProtocolTests + { + protected static readonly TestComposition EditorFeaturesLspComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures; + + protected static readonly TestComposition FeaturesLspComposition = EditorTestCompositions.LanguageServerProtocol; + protected virtual TestComposition Composition => EditorFeaturesLspComposition; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.cs b/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.cs new file mode 100644 index 00000000..e43f1e01 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Import/AbstractLanguageServerProtocolTests.cs @@ -0,0 +1,807 @@ +// modified version of +// https://github.com/dotnet/roslyn/blob/1a4c3f429fe13a2e928c800cebbf93154447095a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +// with sections commented out using /* */ +// also changed "LanguageNames.CSharp" to "LanguageName.MSBuild" + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +/* +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; +using Microsoft.CodeAnalysis.Editor.Test; +using Microsoft.CodeAnalysis.Editor.UnitTests; +using Microsoft.CodeAnalysis.Extensions; +*/ +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +/* +using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; +*/ +using Microsoft.CodeAnalysis.LanguageServer.UnitTests; +/* +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +*/ +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Nerdbank.Streams; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; +using StreamJsonRpc; +using Xunit; +using Xunit.Abstractions; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Roslyn.Test.Utilities +{ + [UseExportProvider] + public abstract partial class AbstractLanguageServerProtocolTests + { + private static readonly SystemTextJsonFormatter s_messageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(); + + private protected readonly AbstractLspLogger TestOutputLspLogger; + protected AbstractLanguageServerProtocolTests(ITestOutputHelper? testOutputHelper) + { + TestOutputLspLogger = testOutputHelper != null ? new TestOutputLspLogger(testOutputHelper) : NoOpLspLogger.Instance; + } + + /* + protected static readonly TestComposition EditorFeaturesLspComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures + .AddParts(typeof(TestDocumentTrackingService)) + .AddParts(typeof(TestWorkspaceRegistrationService)); + + protected static readonly TestComposition FeaturesLspComposition = EditorTestCompositions.LanguageServerProtocol + .AddParts(typeof(TestDocumentTrackingService)) + .AddParts(typeof(TestWorkspaceRegistrationService)); + + private class TestSpanMapperProvider : IDocumentServiceProvider + { + TService? IDocumentServiceProvider.GetService() where TService : class + => typeof(TService) == typeof(ISpanMappingService) ? (TService)(object)new TestSpanMapper() : null; + } + + internal class TestSpanMapper : ISpanMappingService + { + private static readonly LinePositionSpan s_mappedLinePosition = new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 5)); + private static readonly string s_mappedFilePath = "c:\\MappedFile_\ue25b\ud86d\udeac.cs"; + + internal static readonly string GeneratedFileName = "GeneratedFile_\ue25b\ud86d\udeac.cs"; + + internal static readonly LSP.Location MappedFileLocation = new LSP.Location + { + Range = ProtocolConversions.LinePositionToRange(s_mappedLinePosition), + Uri = ProtocolConversions.CreateAbsoluteUri(s_mappedFilePath) + }; + + /// + /// LSP tests are simulating the new razor system which does support mapping import directives. + /// + public bool SupportsMappingImportDirectives => true; + + public Task> MapSpansAsync(Document document, IEnumerable spans, CancellationToken cancellationToken) + { + ImmutableArray mappedResult = default; + if (document.Name == GeneratedFileName) + { + mappedResult = spans.Select(span => new MappedSpanResult(s_mappedFilePath, s_mappedLinePosition, new TextSpan(0, 5))).ToImmutableArray(); + } + + return Task.FromResult(mappedResult); + } + + public Task> GetMappedTextChangesAsync( + Document oldDocument, + Document newDocument, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + */ + + private protected class OrderLocations : Comparer + { + public override int Compare(LSP.Location? x, LSP.Location? y) => CompareLocations(x!, y!); + } + + /* + protected virtual TestComposition Composition => EditorFeaturesLspComposition; + + private protected virtual TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference() + => new(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap()); + */ + + private protected static LSP.ClientCapabilities CapabilitiesWithVSExtensions => new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }; + + private protected static LSP.ClientCapabilities GetCapabilities(bool isVS) + => isVS ? CapabilitiesWithVSExtensions : new LSP.ClientCapabilities(); + + /// + /// Asserts two objects are equivalent by converting to JSON and ignoring whitespace. + /// + /// the expected object to be converted to JSON. + /// the actual object to be converted to JSON. + public static void AssertJsonEquals(T1 expected, T2 actual) + { + var expectedStr = JsonSerializer.Serialize(expected, s_messageFormatter.JsonSerializerOptions); + var actualStr = JsonSerializer.Serialize(actual, s_messageFormatter.JsonSerializerOptions); + AssertEqualIgnoringWhitespace(expectedStr, actualStr); + } + + protected static void AssertEqualIgnoringWhitespace(string expected, string actual) + { + var expectedWithoutWhitespace = Regex.Replace(expected, @"\s+", string.Empty); + var actualWithoutWhitespace = Regex.Replace(actual, @"\s+", string.Empty); + Assert.Equal(expectedWithoutWhitespace, actualWithoutWhitespace); + } + + /// + /// Assert that two location lists are equivalent. + /// Locations are not always returned in a consistent order so they must be sorted. + /// + private protected static void AssertLocationsEqual(IEnumerable expectedLocations, IEnumerable actualLocations) + { + var orderedActualLocations = actualLocations.OrderBy(CompareLocations); + var orderedExpectedLocations = expectedLocations.OrderBy(CompareLocations); + + AssertJsonEquals(orderedExpectedLocations, orderedActualLocations); + } + + private protected static int CompareLocations(LSP.Location l1, LSP.Location l2) + { + var compareDocument = l1.Uri.AbsoluteUri.CompareTo(l2.Uri.AbsoluteUri); + var compareRange = CompareRange(l1.Range, l2.Range); + return compareDocument != 0 ? compareDocument : compareRange; + } + + private protected static int CompareRange(LSP.Range r1, LSP.Range r2) + { + var compareLine = r1.Start.Line.CompareTo(r2.Start.Line); + var compareChar = r1.Start.Character.CompareTo(r2.Start.Character); + return compareLine != 0 ? compareLine : compareChar; + } + + private protected static string ApplyTextEdits(LSP.TextEdit[] edits, SourceText originalMarkup) + { + var text = originalMarkup; + foreach (var edit in edits) + { + var lines = text.Lines; + var startPosition = ProtocolConversions.PositionToLinePosition(edit.Range.Start); + var endPosition = ProtocolConversions.PositionToLinePosition(edit.Range.End); + var textSpan = lines.GetTextSpan(new LinePositionSpan(startPosition, endPosition)); + text = text.Replace(textSpan, edit.NewText); + } + + return text.ToString(); + } + /* + internal static LSP.SymbolInformation CreateSymbolInformation(LSP.SymbolKind kind, string name, LSP.Location location, Glyph glyph, string? containerName = null) + { + var imageId = glyph.GetImageId(); + + var info = new LSP.VSSymbolInformation() + { + Kind = kind, + Name = name, + Location = location, + Icon = new LSP.VSImageId { Guid = imageId.Guid, Id = imageId.Id }, + }; + + if (containerName != null) + info.ContainerName = containerName; + + return info; + } + */ + private protected static LSP.TextDocumentIdentifier CreateTextDocumentIdentifier(Uri uri/*, ProjectId? projectContext = null*/) + { + var documentIdentifier = new LSP.VSTextDocumentIdentifier { Uri = uri }; + /* + if (projectContext != null) + { + documentIdentifier.ProjectContext = + new LSP.VSProjectContext { Id = ProtocolConversions.ProjectIdToProjectContextId(projectContext), Label = projectContext.DebugName!, Kind = LSP.VSProjectKind.CSharp }; + } + */ + return documentIdentifier; + } + + private protected static LSP.TextDocumentPositionParams CreateTextDocumentPositionParams(LSP.Location caret/*, ProjectId? projectContext = null*/) + => new LSP.TextDocumentPositionParams() + { + TextDocument = CreateTextDocumentIdentifier(caret.Uri/*, projectContext*/), + Position = caret.Range.Start + }; + + private protected static LSP.MarkupContent CreateMarkupContent(LSP.MarkupKind kind, string value) + => new LSP.MarkupContent() + { + Kind = kind, + Value = value + }; + + /* + private protected static LSP.CompletionParams CreateCompletionParams( + LSP.Location caret, + LSP.VSInternalCompletionInvokeKind invokeKind, + string triggerCharacter, + LSP.CompletionTriggerKind triggerKind) + => new LSP.CompletionParams() + { + TextDocument = CreateTextDocumentIdentifier(caret.Uri), + Position = caret.Range.Start, + Context = new LSP.VSInternalCompletionContext() + { + InvokeKind = invokeKind, + TriggerCharacter = triggerCharacter, + TriggerKind = triggerKind, + } + }; + + private protected static async Task CreateCompletionItemAsync( + string label, + LSP.CompletionItemKind kind, + string[] tags, + LSP.CompletionParams request, + Document document, + bool preselect = false, + ImmutableArray? commitCharacters = null, + LSP.TextEdit? textEdit = null, + string? textEditText = null, + string? sortText = null, + string? filterText = null, + long resultId = 0, + bool vsResolveTextEditOnCommit = false, + LSP.CompletionItemLabelDetails? labelDetails = null) + { + var position = await document.GetPositionFromLinePositionAsync( + ProtocolConversions.PositionToLinePosition(request.Position), CancellationToken.None).ConfigureAwait(false); + var completionTrigger = await ProtocolConversions.LSPToRoslynCompletionTriggerAsync( + request.Context, document, position, CancellationToken.None).ConfigureAwait(false); + + var item = new LSP.VSInternalCompletionItem() + { + TextEdit = textEdit, + TextEditText = textEditText, + FilterText = filterText, + Label = label, + SortText = sortText, + InsertTextFormat = LSP.InsertTextFormat.Plaintext, + Kind = kind, + Data = JsonSerializer.SerializeToElement(new CompletionResolveData(resultId, ProtocolConversions.DocumentToTextDocumentIdentifier(document)), s_messageFormatter.JsonSerializerOptions), + Preselect = preselect, + VsResolveTextEditOnCommit = vsResolveTextEditOnCommit, + LabelDetails = labelDetails + }; + + if (tags != null) + item.Icon = tags.ToImmutableArray().GetFirstGlyph().GetImageElement().ToLSPImageElement(); + + if (commitCharacters != null) + item.CommitCharacters = commitCharacters.Value.Select(c => c.ToString()).ToArray(); + + return item; + } + */ + + private protected static LSP.TextEdit GenerateTextEdit(string newText, int startLine, int startChar, int endLine, int endChar) + => new LSP.TextEdit + { + NewText = newText, + Range = new LSP.Range + { + Start = new LSP.Position { Line = startLine, Character = startChar }, + End = new LSP.Position { Line = endLine, Character = endChar } + } + }; + + /* + private protected static CodeActionResolveData CreateCodeActionResolveData(string uniqueIdentifier, LSP.Location location, string[] codeActionPath, IEnumerable? customTags = null) + => new(uniqueIdentifier, customTags.ToImmutableArrayOrEmpty(), location.Range, CreateTextDocumentIdentifier(location.Uri), fixAllFlavors: null, nestedCodeActions: null, codeActionPath: codeActionPath); + */ + + private protected Task CreateTestLspServerAsync(string markup, bool mutatingLspWorkspace, LSP.ClientCapabilities clientCapabilities, bool callInitialized = true) + => CreateTestLspServerAsync([markup], LanguageName.MSBuild, mutatingLspWorkspace, new InitializationOptions { ClientCapabilities = clientCapabilities, CallInitialized = callInitialized }); + + private protected Task CreateTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null) + => CreateTestLspServerAsync([markup], LanguageName.MSBuild, mutatingLspWorkspace, initializationOptions, composition); + + private protected Task CreateTestLspServerAsync(string[] markups, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null) + => CreateTestLspServerAsync(markups, LanguageName.MSBuild, mutatingLspWorkspace, initializationOptions, composition); + /* + private protected Task CreateVisualBasicTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null) + => CreateTestLspServerAsync([markup], LanguageNames.VisualBasic, mutatingLspWorkspace, initializationOptions, composition); + */ + + private protected Task CreateTestLspServerAsync( + string[] markups, string languageName, bool mutatingLspWorkspace, InitializationOptions? initializationOptions, TestComposition? composition = null, bool commonReferences = true) + { + var lspOptions = initializationOptions ?? new InitializationOptions(); + + var workspace = CreateWorkspace(lspOptions, workspaceKind: null, mutatingLspWorkspace, composition); + + /* + workspace.InitializeDocuments( + TestWorkspace.CreateWorkspaceElement(languageName, files: markups, fileContainingFolders: lspOptions.DocumentFileContainingFolders, sourceGeneratedFiles: lspOptions.SourceGeneratedMarkups, commonReferences: commonReferences), + openDocuments: false); + */ + + return CreateTestLspServerAsync(workspace, lspOptions, languageName); + } + + private async Task CreateTestLspServerAsync(EditorTestWorkspace workspace, InitializationOptions initializationOptions, string languageName) + { + /* + var solution = workspace.CurrentSolution; + + foreach (var document in workspace.Documents) + { + if (document.IsSourceGenerated) + continue; + + solution = solution.WithDocumentFilePath(document.Id, GetDocumentFilePathFromName(document.Name)); + + var documentText = await solution.GetRequiredDocument(document.Id).GetTextAsync(CancellationToken.None); + solution = solution.WithDocumentText(document.Id, SourceText.From(documentText.ToString(), System.Text.Encoding.UTF8, SourceHashAlgorithms.Default)); + } + + foreach (var project in workspace.Projects) + { + // Ensure all the projects have a valid file path. + solution = solution.WithProjectFilePath(project.Id, GetDocumentFilePathFromName(project.FilePath)); + } + + var analyzerReferencesByLanguage = CreateTestAnalyzersReference(); + if (initializationOptions.AdditionalAnalyzers != null) + analyzerReferencesByLanguage = analyzerReferencesByLanguage.WithAdditionalAnalyzers(languageName, initializationOptions.AdditionalAnalyzers); + + solution = solution.WithAnalyzerReferences(new[] { analyzerReferencesByLanguage }); + await workspace.ChangeSolutionAsync(solution); + + // Important: We must wait for workspace creation operations to finish. + // Otherwise we could have a race where workspace change events triggered by creation are changing the state + // created by the initial test steps. This can interfere with the expected test state. + await WaitForWorkspaceOperationsAsync(workspace); + */ + + return await TestLspServer.CreateAsync(workspace, initializationOptions, TestOutputLspLogger); + } + /* + private protected async Task CreateXmlTestLspServerAsync( + string xmlContent, + bool mutatingLspWorkspace, + string? workspaceKind = null, + InitializationOptions? initializationOptions = null) + { + var lspOptions = initializationOptions ?? new InitializationOptions(); + + var workspace = CreateWorkspace(lspOptions, workspaceKind, mutatingLspWorkspace); + + workspace.InitializeDocuments(XElement.Parse(xmlContent), openDocuments: false); + workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(new[] { CreateTestAnalyzersReference() })); + + // Important: We must wait for workspace creation operations to finish. + // Otherwise we could have a race where workspace change events triggered by creation are changing the state + // created by the initial test steps. This can interfere with the expected test state. + await WaitForWorkspaceOperationsAsync(workspace); + return await TestLspServer.CreateAsync(workspace, lspOptions, TestOutputLspLogger); + } + */ + + internal EditorTestWorkspace CreateWorkspace( + InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace, TestComposition? composition = null) + { + var workspace = new EditorTestWorkspace( + composition ?? Composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(EnableOpeningSourceGeneratedFiles: true), supportsLspMutation: mutatingLspWorkspace); + /* + options?.OptionUpdater?.Invoke(workspace.GetService()); + + workspace.GetService().Register(workspace); + */ + return workspace; + } + + /* + /// + /// Waits for the async operations on the workspace to complete. + /// This ensures that events like workspace registration / workspace changes are processed by the time we exit this method. + /// + protected static async Task WaitForWorkspaceOperationsAsync(EditorTestWorkspace workspace) + { + var workspaceWaiter = GetWorkspaceWaiter(workspace); + await workspaceWaiter.ExpeditedWaitAsync(); + } + + private static IAsynchronousOperationWaiter GetWorkspaceWaiter(EditorTestWorkspace workspace) + { + var operations = workspace.ExportProvider.GetExportedValue(); + return operations.GetWaiter(FeatureAttribute.Workspace); + } + + protected static void AddMappedDocument(Workspace workspace, string markup) + { + var generatedDocumentId = DocumentId.CreateNewId(workspace.CurrentSolution.ProjectIds.First()); + var version = VersionStamp.Create(); + var loader = TextLoader.From(TextAndVersion.Create(SourceText.From(markup), version, TestSpanMapper.GeneratedFileName)); + var generatedDocumentInfo = DocumentInfo.Create( + generatedDocumentId, + TestSpanMapper.GeneratedFileName, + loader: loader, + filePath: $"C:\\{TestSpanMapper.GeneratedFileName}", + isGenerated: true) + .WithDocumentServiceProvider(new TestSpanMapperProvider()); + + var newSolution = workspace.CurrentSolution.AddDocument(generatedDocumentInfo); + workspace.TryApplyChanges(newSolution); + } + + internal static async Task>> GetAnnotatedLocationsAsync(EditorTestWorkspace workspace, Solution solution) + { + var locations = new Dictionary>(); + foreach (var testDocument in workspace.Documents) + { + var document = await solution.GetRequiredDocumentAsync(testDocument.Id, includeSourceGenerated: true, CancellationToken.None); + var text = await document.GetTextAsync(CancellationToken.None); + foreach (var (name, spans) in testDocument.AnnotatedSpans) + { + Contract.ThrowIfNull(document.FilePath); + + var locationsForName = locations.GetValueOrDefault(name, new List()); + locationsForName.AddRange(spans.Select(span => ConvertTextSpanWithTextToLocation(span, text, document.GetURI()))); + + // Linked files will return duplicate annotated Locations for each document that links to the same file. + // Since the test output only cares about the actual file, make sure we de-dupe before returning. + locations[name] = locationsForName.Distinct().ToList(); + } + } + + return locations; + + static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, Uri documentUri) + { + var location = new LSP.Location + { + Uri = documentUri, + Range = ProtocolConversions.TextSpanToRange(span, text), + }; + + return location; + } + } + */ + private protected static LSP.Location GetLocationPlusOne(LSP.Location originalLocation) + { + var newPosition = new LSP.Position { Character = originalLocation.Range.Start.Character + 1, Line = originalLocation.Range.Start.Line }; + return new LSP.Location + { + Uri = originalLocation.Uri, + Range = new LSP.Range { Start = newPosition, End = newPosition } + }; + } + + private static string GetDocumentFilePathFromName(string documentName) + => "C:\\" + documentName; + + private static LSP.DidChangeTextDocumentParams CreateDidChangeTextDocumentParams( + Uri documentUri, + ImmutableArray<(LSP.Range Range, string Text)> changes) + { + var changeEvents = changes.Select(change => new LSP.TextDocumentContentChangeEvent + { + Text = change.Text, + Range = change.Range, + }).ToArray(); + + return new LSP.DidChangeTextDocumentParams() + { + TextDocument = new LSP.VersionedTextDocumentIdentifier + { + Uri = documentUri + }, + ContentChanges = changeEvents + }; + } + + private static LSP.DidOpenTextDocumentParams CreateDidOpenTextDocumentParams(Uri uri, string source, string languageId = "") + => new LSP.DidOpenTextDocumentParams + { + TextDocument = new LSP.TextDocumentItem + { + Text = source, + Uri = uri, + LanguageId = languageId + } + }; + + private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(Uri uri) + => new LSP.DidCloseTextDocumentParams() + { + TextDocument = new LSP.TextDocumentIdentifier + { + Uri = uri + } + }; + + internal sealed class TestLspServer : IAsyncDisposable + { + public readonly EditorTestWorkspace TestWorkspace; + /* + private readonly Dictionary> _locations; + */ + private readonly JsonRpc _clientRpc; + /* + private readonly ICodeAnalysisDiagnosticAnalyzerService _codeAnalysisService; + */ + private readonly RoslynLanguageServer LanguageServer; + + public LSP.ClientCapabilities ClientCapabilities { get; } + + private TestLspServer( + EditorTestWorkspace testWorkspace, + /* + Dictionary> locations, + */ + LSP.ClientCapabilities clientCapabilities, + RoslynLanguageServer target, + Stream clientStream, + object? clientTarget = null, + IJsonRpcMessageFormatter? clientMessageFormatter = null) + { + TestWorkspace = testWorkspace; + ClientCapabilities = clientCapabilities; + /* + _locations = locations; + _codeAnalysisService = testWorkspace.Services.GetRequiredService(); + */ + LanguageServer = target; + + clientMessageFormatter ??= RoslynLanguageServer.CreateJsonMessageFormatter(); + + _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, clientMessageFormatter), clientTarget) + { + ExceptionStrategy = ExceptionProcessing.ISerializable, + }; + /* + // Workspace listener events do not run in tests, so we manually register the lsp misc workspace. + TestWorkspace.GetService().Register(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()); + */ + InitializeClientRpc(); + } + + private void InitializeClientRpc() + { + _clientRpc.StartListening(); + /* + var workspaceWaiter = GetWorkspaceWaiter(TestWorkspace); + Assert.False(workspaceWaiter.HasPendingWork); + */ + } + + internal static async Task CreateAsync(EditorTestWorkspace testWorkspace, InitializationOptions initializationOptions, AbstractLspLogger logger) + { + /* + var locations = await GetAnnotatedLocationsAsync(testWorkspace, testWorkspace.CurrentSolution); + */ + var (clientStream, serverStream) = FullDuplexStream.CreatePair(); + var languageServer = CreateLanguageServer(serverStream, serverStream, testWorkspace, /*initializationOptions.ServerKind*/ WellKnownLspServerKinds.MSBuild, logger); + + var server = new TestLspServer(testWorkspace,/* locations, */initializationOptions.ClientCapabilities, languageServer, clientStream, initializationOptions.ClientTarget, initializationOptions.ClientMessageFormatter); + + if (initializationOptions.CallInitialize) + { + await server.ExecuteRequestAsync(LSP.Methods.InitializeName, new LSP.InitializeParams + { + Capabilities = initializationOptions.ClientCapabilities, + Locale = initializationOptions.Locale, + }, CancellationToken.None); + } + + if (initializationOptions.CallInitialized) + { + await server.ExecuteRequestAsync(LSP.Methods.InitializedName, new LSP.InitializedParams { }, CancellationToken.None); + } + + return server; + } + + internal static async Task CreateAsync(EditorTestWorkspace testWorkspace, LSP.ClientCapabilities clientCapabilities, RoslynLanguageServer target, Stream clientStream) + { + /* + var locations = await GetAnnotatedLocationsAsync(testWorkspace, testWorkspace.CurrentSolution); + */ + var server = new TestLspServer(testWorkspace, /*locations, */ clientCapabilities, target, clientStream); + + await server.ExecuteRequestAsync(LSP.Methods.InitializeName, new LSP.InitializeParams + { + Capabilities = clientCapabilities, + }, CancellationToken.None); + + await server.ExecuteRequestAsync(LSP.Methods.InitializedName, new LSP.InitializedParams { }, CancellationToken.None); + + return server; + } + + private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Stream outputStream, EditorTestWorkspace workspace, WellKnownLspServerKinds serverKind, AbstractLspLogger logger) + { + var capabilitiesProvider = workspace.ExportProvider.GetExportedValue(); + var factory = workspace.ExportProvider.GetExportedValue(); + + var jsonMessageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(); + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter)) + { + ExceptionStrategy = ExceptionProcessing.ISerializable, + }; + + var languageServer = (RoslynLanguageServer)factory.Create(jsonRpc, jsonMessageFormatter.JsonSerializerOptions, capabilitiesProvider, serverKind, logger, workspace.Services.HostServices); + + jsonRpc.StartListening(); + return languageServer; + } + + public async Task ExecuteRequestAsync(string methodName, RequestType request, CancellationToken cancellationToken) where RequestType : class + { + // If creating the LanguageServer threw we might timeout without this. + var result = await _clientRpc.InvokeWithParameterObjectAsync(methodName, request, cancellationToken: cancellationToken).ConfigureAwait(false); + return result; + } + + public async Task ExecuteRequest0Async(string methodName, CancellationToken cancellationToken) + { + // If creating the LanguageServer threw we might timeout without this. + var result = await _clientRpc.InvokeWithParameterObjectAsync(methodName, cancellationToken: cancellationToken).ConfigureAwait(false); + return result; + } + + public Task ExecuteNotificationAsync(string methodName, RequestType request) where RequestType : class + { + return _clientRpc.NotifyWithParameterObjectAsync(methodName, request); + } + + public Task ExecuteNotification0Async(string methodName) + { + return _clientRpc.NotifyWithParameterObjectAsync(methodName); + } + + /* + public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "") + { + if (text == null) + { + // LSP open files don't care about the project context, just the file contents with the URI. + // So pick any of the linked documents to get the text from. + var sourceText = await TestWorkspace.CurrentSolution.GetDocuments(documentUri).First().GetTextAsync(CancellationToken.None).ConfigureAwait(false); + text = sourceText.ToString(); + } + var didOpenParams = CreateDidOpenTextDocumentParams(documentUri, text.ToString(), languageId); + await ExecuteRequestAsync(LSP.Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None); + } + */ + + public Task ReplaceTextAsync(Uri documentUri, params (LSP.Range Range, string Text)[] changes) + { + var didChangeParams = CreateDidChangeTextDocumentParams( + documentUri, + changes.ToImmutableArray()); + return ExecuteRequestAsync(LSP.Methods.TextDocumentDidChangeName, didChangeParams, CancellationToken.None); + } + + public Task InsertTextAsync(Uri documentUri, params (int Line, int Column, string Text)[] changes) + { + return ReplaceTextAsync(documentUri, changes.Select(change => (new LSP.Range + { + Start = new LSP.Position { Line = change.Line, Character = change.Column }, + End = new LSP.Position { Line = change.Line, Character = change.Column } + }, change.Text)).ToArray()); + } + + public Task DeleteTextAsync(Uri documentUri, params (int StartLine, int StartColumn, int EndLine, int EndColumn)[] changes) + { + return ReplaceTextAsync(documentUri, changes.Select(change => (new LSP.Range + { + Start = new LSP.Position { Line = change.StartLine, Character = change.StartColumn }, + End = new LSP.Position { Line = change.EndLine, Character = change.EndColumn } + }, string.Empty)).ToArray()); + } + + public Task CloseDocumentAsync(Uri documentUri) + { + var didCloseParams = CreateDidCloseTextDocumentParams(documentUri); + return ExecuteRequestAsync(LSP.Methods.TextDocumentDidCloseName, didCloseParams, CancellationToken.None); + } + + public async Task ShutdownTestServerAsync() + { + await _clientRpc.InvokeAsync(LSP.Methods.ShutdownName).ConfigureAwait(false); + } + + public async Task ExitTestServerAsync() + { + // Since exit is a notification that disposes of the json rpc stream we cannot wait on the result + // of the request itself since it will throw a ConnectionLostException. + // Instead we wait for the server's exit task to be completed. + await _clientRpc.NotifyAsync(LSP.Methods.ExitName).ConfigureAwait(false); + await LanguageServer.WaitForExitAsync().ConfigureAwait(false); + } + + /* + public IList GetLocations(string locationName) => _locations[locationName]; + + public Solution GetCurrentSolution() => TestWorkspace.CurrentSolution; + */ + + public async Task AssertServerShuttingDownAsync() + { + var queueAccessor = GetQueueAccessor()!.Value; + await queueAccessor.WaitForProcessingToStopAsync().ConfigureAwait(false); + Assert.True(GetServerAccessor().HasShutdownStarted()); + Assert.True(queueAccessor.IsComplete()); + } + /* + internal async Task WaitForDiagnosticsAsync() + { + var listenerProvider = TestWorkspace.GetService(); + + await listenerProvider.GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + await listenerProvider.GetWaiter(FeatureAttribute.SolutionCrawlerLegacy).ExpeditedWaitAsync(); + await listenerProvider.GetWaiter(FeatureAttribute.DiagnosticService).ExpeditedWaitAsync(); + } + + */ + internal RequestExecutionQueue.TestAccessor? GetQueueAccessor() => LanguageServer.GetTestAccessor().GetQueueAccessor(); + /* + internal LspWorkspaceManager.TestAccessor GetManagerAccessor() => GetRequiredLspService().GetTestAccessor(); + + internal LspWorkspaceManager GetManager() => GetRequiredLspService(); + */ + internal AbstractLanguageServer.TestAccessor GetServerAccessor() => LanguageServer.GetTestAccessor(); + + internal T GetRequiredLspService() where T : class, ILspService => LanguageServer.GetTestAccessor().GetRequiredLspService(); + + /* + internal ImmutableArray GetTrackedTexts() => GetManager().GetTrackedLspText().Values.Select(v => v.Text).ToImmutableArray(); + internal Task RunCodeAnalysisAsync(ProjectId? projectId) + => _codeAnalysisService.RunAnalysisAsync(GetCurrentSolution(), projectId, onAfterProjectAnalyzed: _ => { }, CancellationToken.None); + */ + public async ValueTask DisposeAsync() + { + /* + TestWorkspace.GetService().Deregister(TestWorkspace); + TestWorkspace.GetService().Deregister(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()); + */ + // Some tests will manually call shutdown and exit, so attempting to call this during dispose + // will fail as the server's jsonrpc instance will be disposed of. + if (!LanguageServer.GetTestAccessor().HasShutdownStarted()) + { + await ShutdownTestServerAsync(); + await ExitTestServerAsync(); + } + + // Wait for all the exit notifications to run to completion. + await LanguageServer.WaitForExitAsync(); + /* + TestWorkspace.Dispose(); + */ + _clientRpc.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Import/ServerInitializationTests.cs b/MSBuildLanguageServer.Tests/Import/ServerInitializationTests.cs new file mode 100644 index 00000000..9b540fc5 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Import/ServerInitializationTests.cs @@ -0,0 +1,61 @@ +// modified copy of +// https://raw.githubusercontent.com/dotnet/roslyn/044acb4ec888bf080b707e3db6818107e018d80b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +// changed file extension and content of test document + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Roslyn.LanguageServer.Protocol; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public class ServerInitializationTests : AbstractLanguageServerHostTests +{ + public ServerInitializationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + [Fact] + public async Task TestServerHandlesTextSyncRequestsAsync() + { + await using var server = await CreateLanguageServerAsync(); + var document = new VersionedTextDocumentIdentifier { Uri = ProtocolConversions.CreateAbsoluteUri("C:\\\ue25b\ud86d\udeac.csproj") }; + var response = await server.ExecuteRequestAsync(Methods.TextDocumentDidOpenName, new DidOpenTextDocumentParams + { + TextDocument = new TextDocumentItem + { + Uri = document.Uri, + Text = "" + } + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + + response = await server.ExecuteRequestAsync(Methods.TextDocumentDidChangeName, new DidChangeTextDocumentParams + { + TextDocument = document, + ContentChanges = + [ + new TextDocumentContentChangeEvent + { + Range = new Roslyn.LanguageServer.Protocol.Range { Start = new Position(0, 0), End = new Position(0, 0) }, + Text = "" + } + ] + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + + response = await server.ExecuteRequestAsync(Methods.TextDocumentDidCloseName, new DidCloseTextDocumentParams + { + TextDocument = document + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Import/Stubs.cs b/MSBuildLanguageServer.Tests/Import/Stubs.cs new file mode 100644 index 00000000..c1b5a9f1 --- /dev/null +++ b/MSBuildLanguageServer.Tests/Import/Stubs.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// stubs to help imported files work w/o bringing in too many dependencies + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.Test.Utilities +{ + class EditorTestWorkspace + { + TestComposition composition; + string? workspaceKind; + WorkspaceConfigurationOptions configurationOptions; + bool supportsLspMutation; + + public EditorTestWorkspace (TestComposition composition, string? workspaceKind, WorkspaceConfigurationOptions configurationOptions, bool supportsLspMutation) + { + this.composition = composition; + this.workspaceKind = workspaceKind; + this.configurationOptions = configurationOptions; + this.supportsLspMutation = supportsLspMutation; + Services = new (composition.GetHostServices ()); + ExportProvider = composition.ExportProviderFactory.CreateExportProvider (); + } + + public ExportProvider ExportProvider { get; } + public WorkspaceServices Services { get; internal set; } + + public record class WorkspaceServices (HostServices HostServices); + } + + class WorkspaceConfigurationOptions + { + private bool enableOpeningSourceGeneratedFiles; + + public WorkspaceConfigurationOptions (bool EnableOpeningSourceGeneratedFiles) + { + enableOpeningSourceGeneratedFiles = EnableOpeningSourceGeneratedFiles; + } + } +} + +namespace Microsoft.CodeAnalysis.Options +{ + interface IGlobalOptionService + { + } +} + +namespace Microsoft.CodeAnalysis.Remote +{ + class ZZZZZ { } +} + +namespace Microsoft.CodeAnalysis.UnitTests.Remote +{ + class ZZZZZ { } +} + +namespace Roslyn.Test.Utilities +{ + class TestBase { } +} + +namespace Microsoft.CodeAnalysis.UnitTests.Remote +{ + class TestSerializerService + { + [Export (typeof (Factory))] + internal class Factory { } + } +} + +namespace Microsoft.CodeAnalysis.Remote +{ + class BrokeredServiceBase + { + } +} + +namespace Microsoft.CodeAnalysis.Editor.UnitTests +{ + public static class EditorTestCompositions + { + public static TestComposition LanguageServerProtocol { get; } = TestComposition.Empty + .AddAssemblies( + typeof(global::MonoDevelop.MSBuild.Editor.Common.ThisAssembly).Assembly, + typeof(global::MSBuildLanguageServer.ThisAssembly).Assembly + ); + + public static TestComposition LanguageServerProtocolEditorFeatures => LanguageServerProtocol; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/Import/UseExportProviderAttribute.cs b/MSBuildLanguageServer.Tests/Import/UseExportProviderAttribute.cs new file mode 100644 index 00000000..8c1d80cf --- /dev/null +++ b/MSBuildLanguageServer.Tests/Import/UseExportProviderAttribute.cs @@ -0,0 +1,274 @@ +// modified copy of +// https://raw.githubusercontent.com/dotnet/roslyn/1d8582e04c5af87883eab0a5ba7d0788d19a3eb4/src/Workspaces/CoreTestUtilities/MEF/UseExportProviderAttribute.cs +// portions commented out using /* */ comments +// and other changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition.Hosting; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.Composition; +using Roslyn.Test.Utilities; +using Xunit.Sdk; + +namespace Microsoft.CodeAnalysis.Test.Utilities +{ + /// + /// This attribute supports tests that need to use a MEF container () directly or + /// indirectly during the test sequence. It ensures production code uniformly handles the export provider created + /// during a test, and cleans up the state before the test completes. + /// + /// + /// This attribute serves several important functions for tests that use state variables which are otherwise + /// shared at runtime: + /// + /// Ensures implementations all use the same , which is + /// the one created by the test. + /// Clears static cached values in production code holding instances of , or any + /// object obtained from it or one of its related interfaces such as . + /// Isolates tests by waiting for asynchronous operations to complete before a test is considered + /// complete. + /// When required, provides a separate for the + /// executing in the test process. If this provider is created during testing, it is cleaned up with the primary + /// export provider during test teardown. + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class UseExportProviderAttribute : BeforeAfterTestAttribute + { + /// + /// Asynchronous operations are expected to be cancelled at the end of the test that started them. Operations + /// cancelled by the test are cleaned up immediately. The remaining operations are given an opportunity to run + /// to completion. If this timeout is exceeded by the asynchronous operations running after a test completes, + /// the test is failed. + /// + private static readonly TimeSpan CleanupTimeout = TimeSpan.FromMinutes(1); + + private MefHostServices? _hostServices; + + static UseExportProviderAttribute() + { + // Make sure we run the module initializer for Roslyn.Test.Utilities. C# projects do this via a + // build-injected module initializer, but VB projects can ensure initialization occurs by applying the + // UseExportProviderAttribute to test classes that rely on it. + RuntimeHelpers.RunModuleConstructor(typeof(TestBase).Module.ModuleHandle); + + var hostServicesType = typeof(MefHostServices); + var testAccessorType = hostServicesType.GetNestedType("TestAccessor", BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Could not get test accessor type MefHostServices.TestAccessor"); + creationHookType = hostServicesType.GetNestedType("CreationHook", BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Could not get test accessor type MefHostServices.CreationHook"); + testHookMethod = testAccessorType.GetMethod("HookServiceCreation", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException("Could not get test accessor method for MefHostServices.TestAccessor.HookServiceCreation"); + } + + static Type creationHookType; + static MethodInfo testHookMethod; + + static void HookServiceCreationViaReflection(Func, MefHostServices> hook) + { + var handler = new HookHandler(hook); + var creationHook = Delegate.CreateDelegate(creationHookType, handler, typeof(HookHandler).GetMethod("Run")!); + testHookMethod.Invoke(null, [creationHook]); + } + + class HookHandler(Func, MefHostServices> hook) + { + public MefHostServices Run(IEnumerable assemblies) => hook(assemblies); + } + + public override void Before(MethodInfo? methodUnderTest) + { + // Need to clear cached MefHostServices between test runs. + // MODIFICATION: use reflection helper to access internal method + HookServiceCreationViaReflection(CreateMefHostServices); + + // make sure we enable this for all unit tests + AsynchronousOperationListenerProvider.Enable(enable: true, diagnostics: true); + ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(true); + } + + /// + /// To the extent reasonably possible, this method resets the state of the test environment to the same state as + /// it started, ensuring that tests running in sequence cannot influence the outcome of later tests. + /// + /// + /// The test cleanup runs in two primary steps: + /// + /// Waiting for asynchronous operations started by the test to complete. + /// Disposing of mutable resources created by the test. + /// Clearing static state variables related to the use of MEF during a test. + /// + /// + public override void After(MethodInfo? methodUnderTest) + { + try + { + DisposeExportProvider(ExportProviderCache.LocalExportProviderForCleanup); + DisposeExportProvider(ExportProviderCache.RemoteExportProviderForCleanup); + } + finally + { + // Replace hooks with ones that always throw exceptions. These hooks detect cases where code executing + // after the end of a test attempts to create an ExportProvider. + // MODIFICATION: use reflection helper to access internal method + HookServiceCreationViaReflection(DenyMefHostServicesCreationBetweenTests); + + // Reset static state variables. + _hostServices = null; + ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(false); + } + } + + private static void DisposeExportProvider(ExportProvider? exportProvider) + { + if (exportProvider == null) + { + return; + } + + // Dispose of the export provider, including calling Dispose for any IDisposable services created during the test. + using var _ = exportProvider; + + if (exportProvider.GetExportedValues().SingleOrDefault() is { } listenerProvider) + { + // Verify the synchronization context was not used incorrectly + var testExportJoinableTaskContext = exportProvider.GetExportedValues().SingleOrDefault(); + var denyExecutionSynchronizationContext = testExportJoinableTaskContext?.SynchronizationContext as TestExportJoinableTaskContext.DenyExecutionSynchronizationContext; + + // Join remaining operations with a timeout + using (var timeoutTokenSource = new CancellationTokenSource(CleanupTimeout)) + { + if (denyExecutionSynchronizationContext is object) + { + // Immediately cancel the test if the synchronization context is improperly used + denyExecutionSynchronizationContext.InvalidSwitch += delegate { timeoutTokenSource.CancelAfter(0); }; + denyExecutionSynchronizationContext.ThrowIfSwitchOccurred(); + } + + try + { + // This attribute cleans up the in-process and out-of-process export providers separately, so we + // don't need to provide a workspace when waiting for operations to complete. + var waiter = ((AsynchronousOperationListenerProvider)listenerProvider).WaitAllDispatcherOperationAndTasksAsync(workspace: null); + + if (testExportJoinableTaskContext?.DispatcherTaskJoiner is { } taskJoiner) + { + taskJoiner.JoinUsingDispatcher(waiter, timeoutTokenSource.Token); + } + else + { + waiter.GetAwaiter().GetResult(); + } + } + catch (OperationCanceledException ex) when (timeoutTokenSource.IsCancellationRequested) + { + // If the failure was caused by an invalid thread change, throw that exception + denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); + + var messageBuilder = new StringBuilder("Failed to clean up listeners in a timely manner."); + foreach (var token in ((AsynchronousOperationListenerProvider)listenerProvider).GetTokens()) + { + messageBuilder.AppendLine().Append($" {token}"); + } + + throw new TimeoutException(messageBuilder.ToString(), ex); + } + } + + denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); + + foreach (var testErrorHandler in exportProvider.GetExportedValues()) + { + var exceptions = testErrorHandler.Exceptions; + if (exceptions.Count > 0) + { + throw new AggregateException("Tests threw unexpected exceptions", exceptions); + } + } + } + } + + private MefHostServices CreateMefHostServices(IEnumerable assemblies) + { + ExportProvider exportProvider; + + if (assemblies is ImmutableArray array && + array == MefHostServices.DefaultAssemblies && + ExportProviderCache.LocalExportProviderForCleanup != null) + { + if (_hostServices != null) + { + return _hostServices; + } + + exportProvider = ExportProviderCache.LocalExportProviderForCleanup; + } + else + { + exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(assemblies).CreateExportProvider(); + } + + Interlocked.CompareExchange( + ref _hostServices, + new ExportProviderMefHostServices(exportProvider), + null); + + return _hostServices; + } + + private static MefHostServices DenyMefHostServicesCreationBetweenTests(IEnumerable assemblies) + { + // If you hit this, one of three situations occurred: + // + // 1. A test method that uses ExportProvider is not marked with UseExportProviderAttribute (can also be + // applied to the containing type or a base type. + // 2. A test attempted to create an ExportProvider during the test cleanup operations after the + // ExportProvider was already disposed. + // 3. A test attempted to use an ExportProvider in the constructor of the test, or during the initialization + // of a field in the test class. + throw new InvalidOperationException("Cannot create host services after test tear down."); + } + + private class ExportProviderMefHostServices : MefHostServices, IMefHostExportProvider + { + private readonly VisualStudioMefHostServices _vsHostServices; + + public ExportProviderMefHostServices(ExportProvider exportProvider) + : base(new ContainerConfiguration().CreateContainer()) + { + _vsHostServices = VisualStudioMefHostServices.Create(exportProvider); + } + + // MODIFICATION: made protected instead of protected internal, use reflection to call internal method + protected override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace) + { + //=> _vsHostServices.CreateWorkspaceServices(workspace); + return (HostWorkspaceServices?) _vsHostServices + .GetType() + .GetMethod("CreateWorkspaceServices", BindingFlags.NonPublic | BindingFlags.Instance) + ?.Invoke(_vsHostServices, [workspace]) + ?? throw new InvalidOperationException(); + } + + IEnumerable> IMefHostExportProvider.GetExports() + => _vsHostServices.GetExports(); + + IEnumerable> IMefHostExportProvider.GetExports() + => _vsHostServices.GetExports(); + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer.Tests/MSBuildLanguageServer.Tests.csproj b/MSBuildLanguageServer.Tests/MSBuildLanguageServer.Tests.csproj new file mode 100644 index 00000000..5c218340 --- /dev/null +++ b/MSBuildLanguageServer.Tests/MSBuildLanguageServer.Tests.csproj @@ -0,0 +1,70 @@ + + + + net8.0 + enable + enable + MonoDevelop.MSBuild.Editor.LanguageServer.Tests + + $(NoWarn); + + VSTHRD003;VSTHRD103;VSTHRD110;VSTHRD002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MSBuildLanguageServer/.editorconfig b/MSBuildLanguageServer/.editorconfig new file mode 100644 index 00000000..1fe8c7a3 --- /dev/null +++ b/MSBuildLanguageServer/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: http://EditorConfig.org + +[*.cs] + +# revert settings to match roslyn style better +indent_style = space +trim_trailing_whitespace = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_keywords_in_control_flow_statements = false + +# Newline settings +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false + +# VS threading analyzer triggers on imported roslyn code +dotnet_diagnostic.VSTHRD002.severity = none +dotnet_diagnostic.VSTHRD003.severity = none +dotnet_diagnostic.VSTHRD103.severity = none +dotnet_diagnostic.VSTHRD110.severity = none \ No newline at end of file diff --git a/MSBuildLanguageServer/DisplayElementRenderer.cs b/MSBuildLanguageServer/DisplayElementRenderer.cs new file mode 100644 index 00000000..9e1890b0 --- /dev/null +++ b/MSBuildLanguageServer/DisplayElementRenderer.cs @@ -0,0 +1,658 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable annotations + +using System.Text; +using System.Xml; +using System.Xml.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.MSBuild.PackageSearch; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.Xml.Logging; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +using IRoslynSymbol = Microsoft.CodeAnalysis.ISymbol; +using ISymbol = MonoDevelop.MSBuild.Language.ISymbol; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace MonoDevelop.MSBuild.Editor; + +// based on MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs +partial class DisplayElementRenderer +{ + readonly StringBuilder sb = new(); + + readonly HashSet allowedTags; + readonly bool supportsMarkdown, supportsIcons; + readonly ILogger logger; + bool supportsTagSpan, supportsTagCode; + + public DisplayElementRenderer(ILogger logger, ClientCapabilities clientCapabilities, ClientInfo? clientInfo, MarkupKind[]? contentFormats) + { + this.logger = logger; + + allowedTags = clientCapabilities.General?.Markdown?.AllowedTags?.ToHashSet() ?? []; + supportsTagSpan = allowedTags.Contains("span"); + supportsTagCode = allowedTags.Contains("code"); + + supportsMarkdown = contentFormats?.IndexOf(MarkupKind.Markdown) > -1; + + supportsIcons = clientInfo?.Name == "Visual Studio Code"; + } + + void AppendItalic(string text) => sb.Append($"*{text}*"); + + public void NewBlock() + { + sb.AppendLine(); + if(supportsMarkdown) + { + sb.AppendLine(); + } + } + + public async Task GetInfoTooltipElement(SourceText buffer, MSBuildRootDocument doc, ISymbol info, MSBuildResolveResult rr, bool hideDeprecationMessage, CancellationToken token) + { + sb.Clear(); + + if(!WriteNameElement(info)) + { + return null; + } + + if(info is IInferredSymbol) { + NewBlock(); + AppendItalic("(inferred)"); + } + + switch(info.Description.DisplayElement) { + case IRoslynSymbol symbol: + if(await GetDocsXml(symbol, token) is string docsXml && !string.IsNullOrEmpty(docsXml)) + { + try + { + RenderDocsXmlSummaryElement(docsXml); + } catch(Exception ex) + { + LogDocsRenderingError(logger, ex); + } + } + break; + case null: + var descStr = DescriptionFormatter.GetDescription(info, doc, rr); + if(!string.IsNullOrEmpty(descStr)) { + NewBlock(); + // already markdown + // TODO: sanitize + sb.Append(descStr); + } + break; + default: + throw new NotSupportedException(); + } + + if(info is VariableInfo vi && !string.IsNullOrEmpty(vi.DefaultValue)) { + NewBlock(); + sb.Append($"Default value: `{vi.DefaultValue}`"); + } + + /* + var seenIn = GetSeenInElement(buffer, rr, info, doc); + if(seenIn != null) { + elements.Add(seenIn); + } + */ + + if(!hideDeprecationMessage && info.IsDeprecated ()) { + AddDeprecationElement(info); + } + + AddHelpElement(info); + + return sb.ToString(); + } + + void AddDeprecationElement(ISymbol info) + { + if(info.IsDeprecated(out string? deprecationMessage)) { + NewBlock(); + AppendIcon(MSBuildGlyph.Deprecated); + + if(deprecationMessage.StartsWith("Deprecated")) + { + sb.Append(deprecationMessage); + } else + { + sb.Append($"Deprecated: {deprecationMessage}"); + } + + EndDiagnosticElement(); + } + } + + void AddHelpElement(ISymbol info) + { + if(supportsMarkdown && info.HasHelpUrl(out string? helpUrl)) { + NewBlock(); + sb.Append($"[Go to documentation]({helpUrl})"); + } + } + + enum KnownColor + { + Keyword, + Whitespace, + Identifier, + Punctuation, + Parameter, + Type, + Comment + } + + // FIXME these are hardcoded from VS Code default dark theme + static string MapColor(KnownColor color) => color switch { + KnownColor.Keyword => "#569cd6", + KnownColor.Identifier => "#9CDCFE", + KnownColor.Punctuation => "#CCCCCC", + KnownColor.Parameter => "#9CDCFE", + KnownColor.Type => "#4EC9B0", + KnownColor.Comment => "#6A9955", + _ => throw new ArgumentException() + }; + + void BeginVSCodeColorSpan(string vscodeColor) => sb.Append($""); + void BeginHexColorSpan(string color) => sb.Append($""); + + void EndSpan() + { + sb.Append(""); + } + + void AppendColorSpan(KnownColor color, string text) + { + if(supportsMarkdown && supportsTagSpan) + { + var mappedColor = MapColor(color); + BeginHexColorSpan(mappedColor); + sb.Append(text); + EndSpan(); + } else + { + sb.Append(text); + } + } + + void StartSignatureBlock() + { + // FIXME: VS Code strips the style attribute from everything except span, and even then it only allows color/background-color + // so using or
 is the only way to get the editor font.
+        // HOWEVER, they both have downside. 
 is a block element, and is styled with top padding, so looks awful.
+        // And the hover widget override's 's background color so if we use it for the signature, it doesn't match styling of other hovers.
+        if(supportsMarkdown && supportsTagCode)
+        {
+            //sb.Append("
");
+            sb.Append("");
+            //sb.Append("");
+        }
+
+        /*
+        TODO can we use ```msbuild with a hacky match in the grammar?
+        {
+            "match": "\u200c\u200c([a-zA-Z-]+) ([a-zA-Z-]+)(?: \: ([a-zA-Z-]+))?\u200c\u200c",
+            "captures": {
+            "1": { "name": "keyword" },
+            "2": { "name": "variable.other" },
+            "3": { "name": "entity.name.type" }
+            }
+        },
+        */
+    }
+
+    void EndSignatureBlock()
+    {
+        if(supportsMarkdown && supportsTagCode)
+        {
+            //sb.Append("");
+            sb.Append("");
+            //sb.Append("
"); + } + } + + bool WriteNameElement(ISymbol info) + { + var label = DescriptionFormatter.GetTitle(info); + if(label.kind == null) { + return false; + } + + StartSignatureBlock(); + + // the icon is a font so put it inside the so size matches signature text + if (supportsIcons && info.GetGlyph(false) is MSBuildGlyph glyph) + { + AppendIcon(glyph); + } + + AppendColorSpan(KnownColor.Keyword, label.kind); + sb.Append(' '); + AppendColorSpan(KnownColor.Identifier, label.name); + + if(info is FunctionInfo fi) { + if(!fi.IsProperty) { + AppendColorSpan(KnownColor.Punctuation, "("); + + bool first = true; + foreach(var p in fi.Parameters) { + if(first) { + first = false; + } else { + AppendColorSpan(KnownColor.Punctuation, ","); + sb.Append(' '); + } + + AppendColorSpan(KnownColor.Parameter, p.Name); + sb.Append(' '); + AppendColorSpan(KnownColor.Punctuation, ":"); + sb.Append(' '); + AppendColorSpan(KnownColor.Type, p.Type); + } + AppendColorSpan(KnownColor.Punctuation, ")"); + } + } + + if(info is ITypedSymbol typedSymbol) { + var tdesc = typedSymbol.GetTypeDescription(); + if(tdesc.Count > 0) { + var typeInfo = string.Join(" ", tdesc); + sb.Append(' '); + AppendColorSpan(KnownColor.Punctuation, ":"); + sb.Append(' '); + AppendColorSpan(KnownColor.Type, typeInfo); + } + } + + EndSignatureBlock(); + + return true; + } + + /* + ContainerElement GetSeenInElement (ITextBuffer buffer, MSBuildResolveResult rr, ISymbol info, MSBuildRootDocument doc) + { + var seenIn = doc.GetDescendedDocumentsReferencingSymbol (info).ToList (); + if (seenIn.Count == 0) { + return null; + } + + Func shorten = null; + + var elements = new List (); + + int count = 0; + foreach (var s in seenIn) { + if (count == 5) { + elements.Add (new ClassifiedTextElement ( + new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "["), + new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "more in Find References", () => { + NavigationService.FindReferences (buffer, rr); + }), + new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "]") + )); + break; + } + count++; + + // collapse any .. segments + string path = System.IO.Path.GetFullPath (s); + + //factor out some common prefixes into variables + //we do this instead of using the original string, as the result is simpler + //and easier to understand + shorten ??= CreateFilenameShortener (doc.Environment); + var replacement = shorten (path); + if (!replacement.HasValue) { + elements.Add ( + new ClassifiedTextElement ( + new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, path, () => OpenFile (path), path) + ) + ); + continue; + } + + elements.Add (new ClassifiedTextElement ( + new ClassifiedTextRun (PredefinedClassificationTypeNames.SymbolReference, replacement.Value.prefix), + new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, replacement.Value.remaining, () => OpenFile (path), path) + )); + } + + if (elements.Count == 0) { + return null; + } + + elements.Insert (0, new ClassifiedTextElement (new ClassifiedTextRun (PredefinedClassificationTypeNames.Other, "Seen in:"))); + return new ContainerElement (ContainerElementStyle.Stacked, elements); + } + */ + + void AddBreak() => sb.AppendLine("
"); + + public string GetResolvedPathElement(List navs) + { + sb.Clear(); + + if(navs.Count == 1) { + sb.Append("Resolved path: "); + AddFileLink(navs[0].Path); + return sb.ToString(); + } + + sb.Append("Resolved paths:"); + + int i = 0; + foreach(var location in navs) { + AddBreak(); + AddFileLink(location.Path); + if(i == 5) { + AddBreak(); + // TODO: make this a link + sb.Append("[More in Go to Definition]"); + break; + } + } + + return sb.ToString(); + } + + /// + /// Shortens filenames by extracting common prefixes into MSBuild properties. Returns null if the name could not be shortened in this way. + /// + public Func CreateFilenameShortener(IMSBuildEnvironment environment) + { + var prefixes = GetPrefixes(environment); + return s => GetLongestReplacement(s, prefixes); + } + + static List<(string prefix, string subst)> GetPrefixes(IMSBuildEnvironment environment) + { + var list = new List<(string prefix, string subst)>(); + if(environment.ToolsPath is string toolsPath) { + list.Add((toolsPath, $"$({ReservedPropertyNames.binPath})")); + } + + if(environment.ToolsetProperties != null) { + var wellKnownPathProperties = new[] { WellKnownProperties.MSBuildSDKsPath, WellKnownProperties.MSBuildExtensionsPath, WellKnownProperties.MSBuildExtensionsPath32, WellKnownProperties.MSBuildExtensionsPath64 }; + foreach(var propName in wellKnownPathProperties) { + if(environment.ToolsetProperties.TryGetValue(propName, out var propVal)) { + list.Add((propVal, $"$({propName})")); + } + } + } + + return list; + } + + static (string prefix, string remaining)? GetLongestReplacement(string val, List<(string prefix, string subst)> replacements) + { + (string prefix, string subst)? longestReplacement = null; + foreach(var replacement in replacements) { + if(val.StartsWith(replacement.prefix, StringComparison.OrdinalIgnoreCase)) { + if(!longestReplacement.HasValue || longestReplacement.Value.prefix.Length < replacement.prefix.Length) { + longestReplacement = replacement; + } + } + } + + if(longestReplacement.HasValue) { + return (longestReplacement.Value.subst, val.Substring(longestReplacement.Value.prefix.Length)); + } + + return null; + } + + Task GetDocsXml(IRoslynSymbol symbol, CancellationToken token) + { + return Task.Run(() => { + try { + // MSBuild uses property getters directly but they don't typically have docs. + // Use the docs from the property instead. + // FIXME: this doesn't seem to work for the indexer string[]get_Chars, at least on Mono + if(symbol is IMethodSymbol method && method.MethodKind == MethodKind.PropertyGet) { + symbol = method.AssociatedSymbol ?? symbol; + } + return symbol.GetDocumentationCommentXml(expandIncludes: true, cancellationToken: token); + } catch(Exception ex) when(!(ex is OperationCanceledException && token.IsCancellationRequested)) { + LogDocsLoadingError(logger, ex); + } + return null; + }, token); + } + + // roslyn's IDocumentationCommentFormattingService seems to be basically unusable + // without internals access, so do some basic formatting ourselves + bool RenderDocsXmlSummaryElement(string docs) + { + var docsXml = XDocument.Parse(docs); + var summaryEl = docsXml.Root?.Element("summary"); + if(summaryEl == null) { + return false; + } + + AddBreak(); + + foreach(var node in summaryEl.Nodes()) { + switch(node) { + case XText text: + // TODO: escaping + sb.Append(text.Value); + break; + case XElement el: + switch(el.Name.LocalName) { + case "see": + AddTypeNameFromCref(el, logger); + continue; + case "attribution": + continue; + case "para": + NewBlock(); + RenderXmlDocsPara(el, logger); + continue; + default: + LogDocsUnexpectedElement(logger, "summary", el.Name.ToString()); + continue; + } + default: + LogDocsUnexpectedNode(logger, "summary", node.NodeType); + continue; + } + } + + return true; + } + + void AddTypeNameFromCref(XElement el, ILogger logger) + { + if(el.Attribute("cref") is { } att && att.Value is string cref) { + var colonIdx = cref.IndexOf(':'); + if(colonIdx > -1) { + cref = cref.Substring(colonIdx + 1); + } + if(!string.IsNullOrEmpty(cref)) { + AppendColorSpan(KnownColor.Type, cref); + } + } else { + LogDocsMissingAttribute(logger, "see", "cref"); + } + } + + void RenderXmlDocsPara(XElement para, ILogger logger) + { + foreach(var node in para.Nodes()) { + switch(node) { + case XText text: + sb.Append(text.Value); + continue; + case XElement el: + switch(el.Name.LocalName) { + case "see": + AddTypeNameFromCref(el, logger); + continue; + default: + LogDocsUnexpectedElement(logger, "para", el.Name.ToString()); + continue; + } + default: + LogDocsUnexpectedNode(logger, "para", node.NodeType); + continue; + } + } + } + + public string GetPackageInfoTooltip(string packageId, IPackageInfo? package) + { + sb.Clear(); + + StartSignatureBlock(); + + // TODO: GetImageElement (feedKind), + AppendColorSpan(KnownColor.Keyword, "package"); + sb.Append(" "); + AppendColorSpan(KnownColor.Type, package?.Id ?? packageId); + + EndSignatureBlock(); + + NewBlock(); + + if(package is null) + { + AppendColorSpan(KnownColor.Comment, "Could not load package information"); + return sb.ToString(); + } + + var description = !string.IsNullOrWhiteSpace(package.Description) ? package.Description : package.Summary; + if(string.IsNullOrWhiteSpace(description)) { + description = package.Summary; + } + if(!string.IsNullOrWhiteSpace(description)) { + // TODO: VS Code sanitizes this but we should too + sb.Append(description); + } else { + AppendColorSpan(KnownColor.Comment, "[no description]"); + } + + if(!supportsMarkdown) + { + return sb.ToString(); + } + + var nugetOrgUrl = package.GetNuGetOrgUrl(); + if(nugetOrgUrl != null) { + NewBlock(); + AddLink(nugetOrgUrl, "Go to package on NuGet.org"); + } + + var projectUrl = package.ProjectUrl != null && Uri.TryCreate(package.ProjectUrl, UriKind.Absolute, out var parsedUrl) && parsedUrl.Scheme == Uri.UriSchemeHttps + ? package.ProjectUrl : null; + if(projectUrl != null) { + NewBlock(); + AddLink(projectUrl, "Go to project URL"); + } + + return sb.ToString(); + } + + void AddLink(string url, string linkText) + { + if(!supportsMarkdown) + { + throw new NotSupportedException("Cannot add link when markdown is not supported"); + } + sb.Append($"[{Escape(linkText)}]({url})"); + } + + void AddFileLink(string filePath, string? linkText = null) + { + if(!supportsMarkdown) + { + sb.Append(filePath); + return; + } + var fullPath = Path.GetFullPath(filePath); + var uriString = ProtocolConversions.GetAbsoluteUriString(fullPath); + sb.Append($"[{Escape(linkText ?? filePath)}]({uriString})"); + } + + static string Escape(string s) => ProtocolConversions.EscapeMarkdown(s); + + //public object GetDiagnosticTooltip (MSBuildDiagnostic diagnostic) => GetDiagnosticElement (diagnostic.Descriptor.Severity, diagnostic.GetFormattedMessage () ?? diagnostic.GetFormattedTitle ()); + + void AppendIcon(MSBuildGlyph glyph) + { + if(!supportsIcons) + { + return; + } + var iconName = glyph.ToVSCodeImage().ToVSCodeImageId(); + sb.Append($"$({iconName}) "); // icons are text so add a trailing space to separate from following text + } + + void StartDiagnosticElement(MSBuildDiagnosticSeverity severity) + { + AppendIcon(severity.ToGlyph()); + + /* + var imageId = severity switch { + MSBuildDiagnosticSeverity.Error => KnownImages.StatusError, + MSBuildDiagnosticSeverity.Warning => KnownImages.StatusWarning, + _ => KnownImages.StatusInformation + }; + */ + + // should we show the title as well as the description? it's not possible to align the image cleanly if we do that + //var titleElement = new ClassifiedTextElement (new ClassifiedTextRun (PredefinedClassificationTypeNames.NaturalLanguage, diagnostic.GetFormattedTitle (), ClassifiedTextRunStyle.Bold)); + + /* + var messageElements = FormatDescriptionText (message); + var imageElement = GetImageElement (imageId); + + return new ContainerElement ( + ContainerElementStyle.Wrapped | ContainerElementStyle.VerticalPadding, + imageElement, + new ClassifiedTextElement (messageElements) + ); + */ + } + + void EndDiagnosticElement() { } + + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected element '{childElementName}'")] + static partial void LogDocsUnexpectedElement(ILogger logger, string elementName, UserIdentifiable childElementName); + + [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected attribute '{attributeName}'")] + static partial void LogDocsUnexpectedAttribute(ILogger logger, string elementName, UserIdentifiable attributeName); + + [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected attribute '{attributeName}'")] + static partial void LogDocsMissingAttribute(ILogger logger, string elementName, UserIdentifiable attributeName); + + [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = "XML docs element '{elementName}' has unexpected node of type {nodeType}")] + static partial void LogDocsUnexpectedNode(ILogger logger, string elementName, XmlNodeType nodeType); + + [LoggerMessage(EventId = 4, Level = LogLevel.Warning, Message = "Error loading XML docs")] + static partial void LogDocsLoadingError(ILogger logger, Exception ex); + + [LoggerMessage(EventId = 5, Level = LogLevel.Warning, Message = "Error rendering XML docs")] + static partial void LogDocsRenderingError(ILogger logger, Exception ex); +} diff --git a/MSBuildLanguageServer/GlobalSuppressions.cs b/MSBuildLanguageServer/GlobalSuppressions.cs new file mode 100644 index 00000000..64a7b4ab --- /dev/null +++ b/MSBuildLanguageServer/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1024:Symbols should be compared for equality", Justification = "", Scope = "member", Target = "~M:Roslyn.Utilities.GeneratedCodeUtilities.IsGeneratedSymbolWithGeneratedCodeAttribute(Microsoft.CodeAnalysis.ISymbol,Microsoft.CodeAnalysis.INamedTypeSymbol)~System.Boolean")] diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs b/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs new file mode 100644 index 00000000..955d713c --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionClientCapabilities.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// Helper that makes reading client completion capabilities simpler and more performant. +/// +class CompletionClientCapabilities +{ + public static CompletionClientCapabilities Create(ClientCapabilities clientCapabilities) => new(clientCapabilities); + + CompletionClientCapabilities(ClientCapabilities clientCapabilities) + { + var completionSetting = clientCapabilities.TextDocument?.Completion; + + ContextSupport = completionSetting?.ContextSupport ?? false; + + SupportedItemDefaults = completionSetting?.CompletionListSetting?.ItemDefaults?.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? []; + + SupportedItemKinds = completionSetting?.CompletionItemKind?.ValueSet?.ToHashSet() ?? []; + + DefaultInsertTextMode = completionSetting?.InsertTextMode; + + LabelDetailsSupport = completionSetting?.CompletionItem?.LabelDetailsSupport ?? false; + + CommitCharactersSupport = completionSetting?.CompletionItem?.CommitCharactersSupport ?? false; + +#pragma warning disable CS0618 // Type or member is obsolete + DeprecatedSupport = completionSetting?.CompletionItem?.DeprecatedSupport ?? false; +#pragma warning restore CS0618 // Type or member is obsolete + + DocumentationFormat = completionSetting?.CompletionItem?.DocumentationFormat?.ToHashSet() ?? []; + + InsertReplaceSupport = completionSetting?.CompletionItem?.InsertReplaceSupport ?? false; + + InsertTextModeSupport = completionSetting?.CompletionItem?.InsertTextModeSupport?.ValueSet.ToHashSet() ?? []; + + PreselectSupport = completionSetting?.CompletionItem?.PreselectSupport ?? false; + + ResolveSupport = completionSetting?.CompletionItem?.ResolveSupport?.Properties.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? []; + + SnippetSupport = completionSetting?.CompletionItem?.SnippetSupport ?? false; + + TagSupport = completionSetting?.CompletionItem?.TagSupport?.ValueSet.ToHashSet() ?? []; + } + + /// + /// Whether the client supports providing additional context via the property + /// + public bool ContextSupport { get; } + + /// + /// The property names that the client supports on the collection + /// + public HashSet SupportedItemDefaults { get; } + + /// + /// The values supported by the client + /// + public HashSet SupportedItemKinds { get; } + + /// + /// The client's default insertion behavior for items that do not specify a value + /// + public InsertTextMode? DefaultInsertTextMode { get; } + + /// + /// Whether the client supports the property + /// + public bool LabelDetailsSupport { get; } + + /// + /// Whether the client supports the property + /// + public bool CommitCharactersSupport { get; } + + /// + /// Whether the client supports the deprecated property + /// + public bool DeprecatedSupport { get; } + + /// + /// Which formats the client supports for documentation + /// + public HashSet DocumentationFormat { get; } + + /// + /// Whether the client supports values on the property + /// + public bool InsertReplaceSupport { get; } + + /// + /// Whether the client supports the property + /// + public HashSet InsertTextModeSupport { get; } + + /// + /// Whether the client supports the property + /// + public bool PreselectSupport { get; } + + /// + /// Which properties the client supports resolving + /// + public HashSet ResolveSupport { get; } + + /// + /// Whether the client supports treating as a snippet + /// when is set to . + /// + public bool SnippetSupport { get; } + + /// + /// The values supported by the client + /// + public HashSet TagSupport { get; } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs b/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs new file mode 100644 index 00000000..eff48d8c --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionHandler.cs @@ -0,0 +1,429 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Editor.NuGetSearch; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.MSBuild.PackageSearch; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Editor.Completion; +using MonoDevelop.Xml.Parser; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; + +using static MonoDevelop.MSBuild.Language.ExpressionCompletion; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler; + +[ExportCSharpVisualBasicStatelessLspService(typeof(CompletionHandler)), Shared] +[Method(Methods.TextDocumentCompletionName)] +[method: ImportingConstructor] +sealed class CompletionHandler([Import(AllowDefault = true)] IMSBuildFileSystem fileSystem) + : ILspServiceDocumentRequestHandler?> +{ + readonly IMSBuildFileSystem fileSystem = fileSystem ?? DefaultMSBuildFileSystem.Instance; + + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request) => request.TextDocument; + + readonly XmlRootState stateMachine = new(); + + public async Task?> HandleRequestAsync(CompletionParams request, RequestContext context, CancellationToken cancellationToken) + { + // HELP: CompletionTriggerKind maps really poorly + // + // VS has CompletionTriggerReason.Insertion|Backspace|Invoke|InvokeAndCommitIfUnique and a few others + // and XmlEditor maps this to its internal type XmlTriggerReason.TypedChar|Backspace|Invocation. + // + // However, in LSP the distinction is between + // * CompletionTriggerKind.TypedCharacter, which means a character we *explicitly* marked as a trigger char was typed + // * CompletionTriggerKind.Invoked, which means completion was manually invoked or any character was typed + // + // CompletionTriggerKind.TypedCharacter isn't useful to us, and so we only handle Invoked. However, this + // maps to both XmlTriggerReason.TypedChar and XmlTriggerReason.Invocation, which both handle subtly + // different behaviors for implicit vs explicit completion invocation. For now, we will just map it + // to Invocation and review the details later. + + switch(request.Context?.TriggerKind ?? CompletionTriggerKind.Invoked) + { + // user typed [a-zA-Z] or explicitly triggered completion + case CompletionTriggerKind.Invoked: + // user typed a char in ServerCapabilities.CompletionOptions.TriggerCharacters + case CompletionTriggerKind.TriggerCharacter: + // we marked a completion list as incomplete and user typed another char + case CompletionTriggerKind.TriggerForIncompleteCompletions: + break; + // unknown, ignore + default: + return null; + } + + var xmlReason = XmlTriggerReason.Invocation; + var msbuildReason = ExpressionTriggerReason.Invocation; + + var document = context.GetRequiredDocument(); + var sourceText = document.Text.Text; + var textSource = sourceText.GetTextSource(); + var position = ProtocolConversions.PositionToLinePosition(request.Position); + int offset = position.ToOffset(sourceText); + var typedCharacter = offset <= 0 ? '\0' : sourceText[offset - 1]; + + var msbuildParserService = context.GetRequiredService(); + var xmlParserService = context.GetRequiredService(); + var logger = context.GetRequiredService(); + var extLogger = logger.ToILogger(); + + // get a spine, reusing anything we can from the last completed parse + // TODO: port the caching spine parser? + xmlParserService.TryGetCompletedParseResult(document.CurrentState, out var lastXmlParse); + var spine = LspXmlParserService.GetSpineParser(stateMachine, lastXmlParse, position, sourceText, cancellationToken); + + // TODO : replace this with collection of schemas + async Task GetRootDocument() + { + if(GetMSBuildParseResult(msbuildParserService, document.CurrentState, cancellationToken) is { } t) + { + return (await t).MSBuildDocument; + } else + { + return MSBuildRootDocument.Empty; + } + } + + var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider; + + //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards + await functionTypeProvider.EnsureInitialized(cancellationToken); + + var msbuildTrigger = MSBuildCompletionTrigger.TryCreate(spine, textSource, msbuildReason, offset, typedCharacter, extLogger, functionTypeProvider, null, cancellationToken); + if(msbuildTrigger is not null) + { + MSBuildRootDocument doc = await GetRootDocument(); + return await GetExpressionCompletionList(request, context, doc, msbuildTrigger, extLogger, sourceText, functionTypeProvider, fileSystem, cancellationToken).ConfigureAwait(false); + } + + (XmlCompletionTrigger kind, int spanStart, int spanLength)? xmlTrigger = null; + xmlTrigger = XmlCompletionTriggering.GetTriggerAndSpan(spine, xmlReason, typedCharacter, textSource, cancellationToken: cancellationToken); + if(xmlTrigger.Value.kind != XmlCompletionTrigger.None) + { + // ignore the case where the method returns false when the deepest node's name cannot be completed, we will try to provide completion anyways + spine.TryGetNodePath(textSource, out var nodePath, cancellationToken: cancellationToken); + + // if we're completing an existing element, remove it from the path + // so we don't get completions for its children instead + if((xmlTrigger.Value.kind == XmlCompletionTrigger.ElementName || xmlTrigger.Value.kind == XmlCompletionTrigger.Tag) && nodePath?.Count > 0) + { + if(nodePath[nodePath.Count - 1] is XElement leaf && leaf.Name.Length == xmlTrigger.Value.spanLength) + { + nodePath.RemoveAt(nodePath.Count - 1); + } + } + + MSBuildRootDocument doc = await GetRootDocument(); + + var clientCapabilities = context.GetRequiredClientCapabilities(); + var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo; + + var editRange = sourceText.GetLspRange(xmlTrigger.Value.spanStart, xmlTrigger.Value.spanLength); + + var rr = MSBuildResolver.Resolve(spine.Clone(), textSource, MSBuildRootDocument.Empty, null, extLogger, cancellationToken); + var docsProvider = MSBuildCompletionDocsProvider.Create(extLogger, clientCapabilities, clientInfo, doc, sourceText, rr); + var xmlCompletionContext = new MSBuildXmlCompletionContext(spine, xmlTrigger.Value.kind, textSource, nodePath, editRange, rr, doc, docsProvider, sourceText); + var dataSource = new MSBuildXmlCompletionDataSource(); + return await GetXmlCompletionListAsync(context, dataSource, xmlCompletionContext, request.TextDocument, cancellationToken).ConfigureAwait(false); + } + + return null; + } + + async static Task GetXmlCompletionListAsync(RequestContext context, MSBuildXmlCompletionDataSource dataSource, MSBuildXmlCompletionContext xmlCompletionContext, TextDocumentIdentifier textDocument, CancellationToken cancellationToken) + { + var completionTasks = dataSource.GetCompletionTasks(xmlCompletionContext, cancellationToken); + + var subLists = await Task.WhenAll(completionTasks).ConfigureAwait(false); + + var allItems = subLists.SelectMany(s => s is null ? [] : s); + + return await CompletionRenderer.RenderCompletionItems(context, textDocument, allItems, xmlCompletionContext.EditRange, cancellationToken).ConfigureAwait(false); + } + + static Task? GetMSBuildParseResult(LspMSBuildParserService msbuildParserService, EditorDocumentState documentState, CancellationToken cancellationToken) + { + // if we determined we are triggering, now we can look up the current parsed document + // FIXME: do we need it to be current or will a stale one do? it's really just used for schemas + if(msbuildParserService.TryGetCompletedParseResult(documentState, out var parseResult)) + { + return Task.FromResult(parseResult); + } else if(msbuildParserService.TryGetParseResult(documentState, out Task? parseTask, cancellationToken)) + { + return parseTask; + } else + { + return null; + } + } + + async static Task GetExpressionCompletionList( + CompletionParams request, RequestContext context, + MSBuildRootDocument doc, MSBuildCompletionTrigger trigger, + ILogger logger, SourceText sourceText, + IFunctionTypeProvider functionTypeProvider, IMSBuildFileSystem fileSystem, + CancellationToken cancellationToken) + { + var rr = trigger.ResolveResult; + + var clientCapabilities = context.GetRequiredClientCapabilities(); + var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo; + var docsProvider = MSBuildCompletionDocsProvider.Create(logger, clientCapabilities, clientInfo, doc, sourceText, rr); + + var valueSymbol = rr.GetElementOrAttributeValueInfo(doc); + if(valueSymbol is null || valueSymbol.ValueKind == MSBuildValueKind.Nothing) + { + return null; + } + + var kindWithModifiers = valueSymbol.ValueKind; + + if(!ValidateListPermitted(trigger.ListKind, kindWithModifiers)) + { + return null; + } + + var kind = kindWithModifiers.WithoutModifiers(); + + if(kind == MSBuildValueKind.Data || kind == MSBuildValueKind.Nothing) + { + return null; + } + + // FIXME: This is a temporary hack so we have completion for imported XSD schemas with missing type info. + // It is not needed for inferred schemas, as they have already performed the inference. + if(kind == MSBuildValueKind.Unknown) + { + kind = MSBuildInferredSchema.InferValueKindFromName(valueSymbol); + } + + var items = await GetExpressionCompletionItems(doc, trigger, logger, functionTypeProvider, fileSystem, rr, docsProvider, valueSymbol, kind, cancellationToken).ConfigureAwait(false); + + bool isIncomplete = false; + + // TODO: use request.PartialResultToken to report these as they come in + if(trigger.TriggerState == TriggerState.Value) + { + switch(kind) + { + case MSBuildValueKind.NuGetID: + { + isIncomplete = true; + if(trigger.Expression is ExpressionText t) + { + var packageSearchManager = context.GetRequiredLspService(); + var packageType = valueSymbol.CustomType?.Values[0].Name; + var packageNameItems = await GetPackageNameCompletions(t.Value, packageType, doc, packageSearchManager, docsProvider, cancellationToken); + if(packageNameItems != null) + { + items.AddRange(packageNameItems); + } + } + break; + } + case MSBuildValueKind.NuGetVersion: + { + isIncomplete = true; + var packageSearchManager = context.GetRequiredLspService(); + var packageVersionItems = await GetPackageVersionCompletion(doc, rr, packageSearchManager, docsProvider, cancellationToken); + if(packageVersionItems != null) + { + items.AddRange(packageVersionItems); + } + break; + } + } + } + + var renderedList = await CompletionRenderer.RenderCompletionItems( + context, + request.TextDocument, + items, + sourceText.GetLspRange(trigger.SpanStart, trigger.SpanLength), + cancellationToken + ).ConfigureAwait(false); + + if (renderedList is not null) + { + renderedList.IsIncomplete = isIncomplete; + } + + return renderedList; + } + + private static async Task> GetExpressionCompletionItems( + MSBuildRootDocument doc, MSBuildCompletionTrigger trigger, ILogger logger, + IFunctionTypeProvider functionTypeProvider, IMSBuildFileSystem fileSystem, + MSBuildResolveResult rr, MSBuildCompletionDocsProvider docsProvider, + ITypedSymbol valueSymbol, MSBuildValueKind kind, + CancellationToken cancellationToken) + { + var items = new List(); + + bool isValue = trigger.TriggerState == TriggerState.Value; + + if(trigger.ComparandVariables != null && isValue) + { + foreach(var ci in GetComparandCompletions(doc, fileSystem, trigger.ComparandVariables, logger)) + { + items.Add(new MSBuildCompletionItem(ci, XmlCommitKind.AttributeValue, docsProvider)); + } + } + + if(isValue) + { + switch(kind) + { + case MSBuildValueKind.Sdk: + case MSBuildValueKind.SdkWithVersion: + { + items.AddRange(SdkCompletion.GetSdkCompletions(doc, logger, cancellationToken).Select(s => new MSBuildSdkCompletionItem(s, docsProvider))); + break; + } + case MSBuildValueKind.Lcid: + items.AddRange(CultureHelper.GetKnownCultures().Select(c => new MSBuildLcidCompletionItem(c, docsProvider))); + break; + case MSBuildValueKind.Culture: + items.AddRange(CultureHelper.GetKnownCultures().Select(c => new MSBuildCultureCompletionItem(c, docsProvider))); + break; + } + + if(kind == MSBuildValueKind.Guid || valueSymbol.CustomType is CustomTypeInfo { BaseKind: MSBuildValueKind.Guid, AllowUnknownValues: true }) + { + items.Add(new MSBuildNewGuidCompletionItem()); + } + } + + //TODO: better metadata support + // NOTE: can't just check CustomTypeInfo isn't null, must check kind, as NuGetID stashes the dependency type in the CustomTypeInfo + if(kind == MSBuildValueKind.CustomType && valueSymbol.CustomType != null && valueSymbol.CustomType.Values.Count > 0 && isValue) + { + bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions(valueSymbol); + foreach(var value in valueSymbol.CustomType.Values) + { + items.Add(new MSBuildCompletionItem(value, XmlCommitKind.AttributeValue, docsProvider, addDescriptionHint: addDescriptionHint)); + } + + } else + { + //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards + await functionTypeProvider.EnsureInitialized(cancellationToken); + if(GetCompletionInfos(rr, trigger.TriggerState, valueSymbol, trigger.Expression, trigger.SpanLength, doc, functionTypeProvider, fileSystem, logger, kindIfUnknown: kind) is IEnumerable completionInfos) + { + bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions(valueSymbol); + foreach(var ci in completionInfos) + { + items.Add(new MSBuildCompletionItem(ci, XmlCommitKind.AttributeValue, docsProvider, addDescriptionHint: addDescriptionHint)); + } + } + } + + bool allowExpressions = valueSymbol.ValueKind.AllowsExpressions(); + + if(allowExpressions && isValue || trigger.TriggerState == TriggerState.BareFunctionArgumentValue) + { + items.Add(new MSBuildReferenceExpressionCompletionItem("$(", "Property reference", MSBuildCompletionItemKind.PropertySyntax)); + } + + if(allowExpressions && isValue) + { + items.Add(new MSBuildReferenceExpressionCompletionItem("@(", "Item reference", MSBuildCompletionItemKind.ItemSyntax)); + if(CompletionHelpers.IsMetadataAllowed(trigger.Expression, rr)) + { + items.Add(new MSBuildReferenceExpressionCompletionItem("%(", "Metadata reference", MSBuildCompletionItemKind.MetadataSyntax)); + } + } + + return items; + } + + static async Task?> GetPackageVersionCompletion(MSBuildRootDocument doc, MSBuildResolveResult resolveResult, IPackageSearchManager packageSearchManager, MSBuildCompletionDocsProvider docsProvider, CancellationToken cancellationToken) + { + if(!PackageCompletion.TryGetPackageVersionSearchJob(resolveResult, doc, packageSearchManager, out var packageSearchJob, out string? packageId, out string? targetFrameworkSearchParameter)) + { + return null; + } + + var results = await packageSearchJob.ToTask(cancellationToken); + + var packageDocsProvider = new PackageCompletionDocsProvider(packageSearchManager, docsProvider, targetFrameworkSearchParameter); + + // FIXME should we deduplicate? + // FIXME: this index sort hack won't work when we are returning the results in parts as they come in from the different sources + var items = new List(); + var index = results.Count; + foreach(var result in results) + { + items.Add(new OrderedPackageVersionCompletionItem (index--, packageId, result, packageDocsProvider)); + } + + return items; + } + + static async Task> GetPackageNameCompletions(string searchString, string? packageType, MSBuildRootDocument doc, IPackageSearchManager packageSearchManager, MSBuildCompletionDocsProvider docsProvider, CancellationToken cancellationToken) + { + // NOTE: empty search string is still valid, it populates the list with some basic/placeholder packages + + var targetFrameworkSearchParameter = doc.GetTargetFrameworkNuGetSearchParameter(); + + var packageDocsProvider = new PackageCompletionDocsProvider(packageSearchManager, docsProvider, targetFrameworkSearchParameter); + + var results = await packageSearchManager.SearchPackageNames(searchString.ToLower(), targetFrameworkSearchParameter, packageType).ToTask(cancellationToken); + + return CreateNuGetItemsFromSearchResults(results, packageDocsProvider); + } + + static List CreateNuGetItemsFromSearchResults(IReadOnlyList> results, PackageCompletionDocsProvider packageDocsProvider) + { + var items = new List(); + var dedup = new HashSet(); + + // dedup, preferring nuget -> myget -> local + AddItems(FeedKind.NuGet); + AddItems(FeedKind.MyGet); + AddItems(FeedKind.Local); + + void AddItems(FeedKind kind) + { + foreach(var result in results) + { + if(result.Item2 == kind) + { + if(dedup.Add(result.Item1)) + { + items.Add(new PackageNameCompletionItem (result, packageDocsProvider)); + } + } + } + } + + return items; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs new file mode 100644 index 00000000..5aede805 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCompletionItem.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Schema; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildCompletionItem( + ISymbol symbol, XmlCommitKind xmlCommitKind, + MSBuildCompletionDocsProvider docsProvider, + string? prefix = null, string? annotation = null, string? sortText = null, bool addDescriptionHint = false + ) : ILspCompletionItem +{ + string label => prefix is not null ? prefix + symbol.Name : symbol.Name; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label, SortText = sortText }; + + if(settings.IncludeDeprecatedPropertyOrTag && symbol.IsDeprecated()) + { + settings.SetDeprecated(item); + } + + if(settings.IncludeItemKind) + { + item.Kind = symbol.GetCompletionItemKind(); + } + + if(annotation is not null) + { + item.FilterText = $"{symbol.Name} {annotation}"; + if(settings.IncludeLabelDetails) + { + item.LabelDetails = new CompletionItemLabelDetails { Description = annotation }; + } + } else if(addDescriptionHint) + { + if(settings.IncludeLabelDetails) + { + var descriptionHint = DescriptionFormatter.GetCompletionHint(symbol); + item.LabelDetails = new CompletionItemLabelDetails { Description = descriptionHint }; + } + } + + // TODO: generate completion edit based on the xmlCompletionItemKind + + if(settings.IncludeDocumentation) + { + var tooltipContent = await docsProvider.GetDocumentation(symbol, cancellationToken); + if(tooltipContent is not null) + { + //Value = "$(symbol-keyword) keyword Choose\r\n\r\nGroups When and Otherwise elements", // tooltipContent, + item.Documentation = tooltipContent; + } + } + + return item; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs new file mode 100644 index 00000000..fbf25914 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildCultureCompletionItem.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Language; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildCultureCompletionItem(KnownCulture culture, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem +{ + string label => culture.Name; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label }; + + if(settings.IncludeItemKind) + { + item.Kind = MSBuildCompletionItemKind.Culture; + } + + // add the culture name to the filter text so ppl can just type the actual language/country instead of looking up the code + item.FilterText = $"{culture.Name} {culture.DisplayName}"; + + if(settings.IncludeLabelDetails) + { + item.LabelDetails = new CompletionItemLabelDetails { Description = culture.DisplayName }; + } + + if(settings.IncludeDocumentation) + { + item.Documentation = await docsProvider.GetDocumentation(culture.CreateCultureSymbol(), cancellationToken); + } + + return item; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs new file mode 100644 index 00000000..72b95f5e --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildLcidCompletionItem.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Language; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildLcidCompletionItem(KnownCulture culture, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem +{ + readonly string label = culture.Lcid.ToString(); + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label }; + + if(settings.IncludeItemKind) + { + item.Kind = MSBuildCompletionItemKind.Culture; + } + + // add the culture name to the filter text so ppl can just type the actual language/country instead of looking up the code + item.FilterText = $"{label} {culture.DisplayName}"; + + if(settings.IncludeLabelDetails) + { + item.LabelDetails = new CompletionItemLabelDetails { Description = culture.DisplayName }; + } + + if(settings.IncludeDocumentation) + { + item.Documentation = await docsProvider.GetDocumentation(culture.CreateCultureSymbol(), cancellationToken); + } + + return item; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs new file mode 100644 index 00000000..b7bfd8d6 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildNewGuidCompletionItem.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildNewGuidCompletionItem() : ILspCompletionItem +{ + const string label = "New GUID"; + + public bool IsMatch(CompletionItem request) => string.Equals(label, request.Label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label }; + + if(settings.IncludeItemKind) + { + item.Kind = MSBuildCompletionItemKind.NewGuid; + } + + if(settings.IncludeDocumentation) + { + item.Documentation = new MarkupContent { + Value = "Inserts a new GUID", + Kind = MarkupKind.Markdown + }; + } + + if(settings.IncludeInsertText) + { + item.InsertText = Guid.NewGuid().ToString("B").ToUpper(); + } + + return item; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs new file mode 100644 index 00000000..783f2608 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildReferenceExpressionCompletionItem.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildReferenceExpressionCompletionItem(string text, string description, CompletionItemKind itemKind) : ILspCompletionItem +{ + public bool IsMatch(CompletionItem request) => string.Equals(text, request.Label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = text }; + + if(settings.IncludeItemKind) + { + item.Kind = itemKind; + } + + if(settings.IncludeDocumentation) + { + item.Documentation = new MarkupContent { + Value = description, + Kind = MarkupKind.Markdown + }; + } + + if (settings.SupportSnippetFormat && settings.IncludeInsertTextFormat && settings.IncludeTextEdit) + { + if (settings.IncludeInsertTextFormat) + { + item.InsertTextFormat = InsertTextFormat.Snippet; + } + // TODO: calculate whether this would unbalance the expression + item.TextEdit = new TextEdit { + NewText = $"{text}$0)", + Range = ctx.EditRange + }; + } + + //TODO: custom commit support. we should be retriggering completion and enabling overtype support for the paren. + //See MSBuildCompletionCommitManager. TryCommitItemKind + + return item; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs new file mode 100644 index 00000000..b175b32f --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/MSBuildSdkCompletionItem.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.SdkResolution; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class MSBuildSdkCompletionItem(SdkInfo info, MSBuildCompletionDocsProvider docsProvider) : ILspCompletionItem +{ + string label => info.Name; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label }; + + if(settings.IncludeItemKind) + { + item.Kind = MSBuildCompletionItemKind.Sdk; + } + + if(settings.IncludeDocumentation && info.Path is string sdkPath) + { + // FIXME: better docs + item.Documentation = new MarkupContent { Kind = MarkupKind.Markdown, Value = $"`{sdkPath}`" }; + } + + return item; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs new file mode 100644 index 00000000..20d0e086 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/OrderedPackageVersionCompletionItem.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class OrderedPackageVersionCompletionItem( + int index, + string packageId, + Tuple packageVersionAndKind, + PackageCompletionDocsProvider docsProvider + ) : ILspCompletionItem +{ + string packageVersion => packageVersionAndKind.Item1; + FeedKind packageKind => packageVersionAndKind.Item2; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, packageVersion, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { + Label = packageVersion, + SortText = $"_{index:D5}" + }; + + if(settings.IncludeItemKind) + { + item.Kind = packageKind.GetCompletionItemKind(); + } + + // TODO: generate completion edit + + if(settings.IncludeDocumentation) + { + item.Documentation = await docsProvider.GetPackageDocumentation(packageId, packageVersion, packageKind, cancellationToken); + } + + return item; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs new file mode 100644 index 00000000..2483192a --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/PackageCompletionItem.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class PackageNameCompletionItem( + Tuple packageNameAndKind, + PackageCompletionDocsProvider docsProvider + ) : ILspCompletionItem +{ + string packageId => packageNameAndKind.Item1; + FeedKind packageKind => packageNameAndKind.Item2; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, packageId, StringComparison.Ordinal); + + public async ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = packageId }; + + if(settings.IncludeItemKind) + { + item.Kind = packageKind.GetCompletionItemKind(); + } + + // TODO: generate completion edit + + if(settings.IncludeDocumentation) + { + item.Documentation = await docsProvider.GetPackageDocumentation(packageId, null, packageKind, cancellationToken); + } + + return item; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs new file mode 100644 index 00000000..488279dd --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlClosingTagCompletionItem.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.Xml.Dom; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class XmlClosingTagCompletionItem(bool includeBracket, string name, XElement element, int dedupCount) : ILspCompletionItem +{ + readonly string label = (includeBracket ? " string.Equals(request.Label, label, StringComparison.Ordinal); + + // TODO: custom insert text, including for multiple closing tags + public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + // force these to sort last, they're not very interesting values to browse as these tags are usually already closed + string sortText = "ZZZZZZ" + label; + + var item = new CompletionItem { Label = label, Kind = XmlToLspCompletionItemKind.ClosingTag, SortText = sortText }; + + if(settings.IncludeDocumentation) + { + item.Documentation = GetClosingTagDocumentation(element, dedupCount > 1); + }; + + return new(item); + } + + + static MarkupContent GetClosingTagDocumentation(XElement element, bool isMultiple) + => CreateMarkdown( + isMultiple + ? $"Closing tag for element `{element.Name}`, closing all intermediate elements" + : $"Closing tag for element `{element.Name}`" + ); + + static MarkupContent CreateMarkdown(string markdown) => new() { Kind = MarkupKind.Markdown, Value = markdown }; +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs new file mode 100644 index 00000000..dad38c98 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlCompletionItem.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class XmlCompletionItem(string label, CompletionItemKind kind, string markdownDocumentation, XmlCommitKind commitKind) : ILspCompletionItem +{ + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label, Kind = kind }; + + if(settings.IncludeDocumentation) + { + // TODO: strip markdown if client only supports text + item.Documentation = markdownDocumentation; + } + + // TODO: generate completion edit based on CommitKind + + return new(item); + } + + static MarkupContent CreateMarkdown(string markdown) + => new() { Kind = MarkupKind.Markdown, Value = markdown }; +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs new file mode 100644 index 00000000..55e61199 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionItems/XmlEntityCompletionItem.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; + +class XmlEntityCompletionItem(string name, string character) : ILspCompletionItem +{ + readonly string label = $"&{name};"; + + public bool IsMatch(CompletionItem request) => string.Equals(request.Label, label, StringComparison.Ordinal); + + + //TODO: need to tweak semicolon insertion for XmlCompletionItemKind.Entity + public ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken) + { + var item = new CompletionItem { Label = label, FilterText = name, Kind = XmlToLspCompletionItemKind.Entity }; + + if(settings.IncludeDocumentation) + { + item.Documentation = $"Escaped '{character}'"; + }; + + return new(item); + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs b/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs new file mode 100644 index 00000000..cf7ca211 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionRenderSettings.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// Provides with information about which properties it should include +/// when creating instances for either the completion list for for resolving individual items. +/// +class CompletionRenderSettings +{ + public CompletionRenderSettings(CompletionClientCapabilities clientCapabilities, bool fullRender) + { + ClientCapabilities = clientCapabilities; + FullRender = fullRender; + + IncludeLabelDetails = ClientCapabilities.LabelDetailsSupport && (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.LabelDetails))); + IncludeItemKind = fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Kind)); + + bool supportsDeprecatedTag = clientCapabilities.TagSupport.Contains(CompletionItemTag.Deprecated); + IncludeDeprecatedTag = supportsDeprecatedTag && (fullRender || !ClientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Tags))); +#pragma warning disable CS0618 // Type or member is obsolete + IncludeDeprecatedProperty = !supportsDeprecatedTag && ClientCapabilities.DeprecatedSupport && (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.Deprecated))); +#pragma warning restore CS0618 // Type or member is obsolete + + IncludeTextEdit = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.TextEdit))); + IncludeInsertText = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.InsertText))); + IncludeTextEditText = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.TextEditText))); + IncludeInsertTextFormat = (fullRender || !clientCapabilities.ResolveSupport.Contains(nameof(CompletionItem.InsertTextFormat))); + + SupportsDataDefault = clientCapabilities.SupportedItemDefaults.Contains(nameof(CompletionListItemDefaults.Data)); + SupportsEditRange = clientCapabilities.SupportedItemDefaults.Contains(nameof(CompletionListItemDefaults.EditRange)); + } + + public CompletionClientCapabilities ClientCapabilities { get; } + + public bool FullRender { get; } + + public bool IncludeDocumentation => FullRender; + + public bool IncludeItemKind { get; } + + public bool IncludeLabelDetails { get; } + + public bool IncludeTextEdit { get; } + + public bool IncludeTextEditText { get; } + + public bool IncludeInsertText { get; } + + public bool IncludeInsertTextFormat { get; } + + public bool SupportsInsertReplaceEdit => ClientCapabilities.InsertReplaceSupport; + + public bool SupportsDataDefault { get; } + + public bool SupportsEditRange { get; } + + /// + /// Whether the client supports treating as a snippet + /// when is set to . + /// + public bool SupportSnippetFormat => ClientCapabilities.SnippetSupport; + + public bool IncludeDeprecatedProperty { get; } + + public bool IncludeDeprecatedTag { get; } + + public bool IncludeDeprecatedPropertyOrTag => IncludeDeprecatedProperty || IncludeDeprecatedTag; + + static readonly CompletionItemTag[] DeprecatedTag = [CompletionItemTag.Deprecated]; + + /// + /// Marks an item as deprecated using whichever method the client supports. + /// + public void SetDeprecated(CompletionItem item) + { + if(IncludeDeprecatedTag) + { + if(item.Tags is not null) + { + throw new ArgumentException("LSP protocol only defines one tag"); + } + item.Tags = DeprecatedTag; + } else if(IncludeDeprecatedProperty) + { +#pragma warning disable CS0618 // Type or member is obsolete + item.Deprecated = true; +#pragma warning restore CS0618 // Type or member is obsolete + } + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs b/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs new file mode 100644 index 00000000..18bff9e5 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionRenderer.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.PooledObjects; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; + +using CompletionResolveData = Microsoft.CodeAnalysis.LanguageServer.Handler.Completion.CompletionResolveData; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +static class CompletionRenderer +{ + public static async Task RenderCompletionItems( + RequestContext context, + TextDocumentIdentifier textDocument, + IEnumerable items, + LSP.Range editRange, + CancellationToken cancellationToken) + { + var clientCapabilities = context.GetRequiredClientCapabilities(); + + var completionCapabilities = CompletionClientCapabilities.Create(clientCapabilities); + var renderSettings = new CompletionRenderSettings(completionCapabilities, false); + + var completionListCache = context.GetRequiredService(); + + var rawItems = new List(); + var renderContext = new CompletionRenderContext(editRange); + var resultId = completionListCache.UpdateCache(new CompletionListCacheEntry(rawItems, renderContext)); + var resolveData = new CompletionResolveData(resultId, textDocument); + + using var _ = ArrayBuilder.GetInstance(out var renderedBuilder); + + // If the client doesn't support EditRange, and the item doesn't define a TextEdit, we must add a computed one. + // If the client doesn't support resolving the TextEdit in the resovle handler, then we must do this upfront. + bool includeEditRangeTextEdit = !renderSettings.SupportsEditRange && renderSettings.IncludeTextEdit; + + // could consider parallelizing this + // however, the common case should be that Render returns a completed ValueTask, so it's faster than it looks + foreach(var item in items) + { + rawItems.Add(item); + + var renderedItem = await item.Render(renderSettings, renderContext, cancellationToken).ConfigureAwait(false); + + if(!renderSettings.SupportsDataDefault) + { + renderedItem.Data = resolveData; + } + + if(includeEditRangeTextEdit) + { + renderedItem.AddEditRangeTextEdit(renderSettings, renderContext); + } + + renderedBuilder.Add(renderedItem); + } + + var completionList = new CompletionList { + Items = renderedBuilder.ToArray() + }; + + + if(renderSettings.SupportsDataDefault) + { + (completionList.ItemDefaults ??= new()).Data = resolveData; + } + + + if(renderSettings.SupportsEditRange) + { + (completionList.ItemDefaults ??= new()).EditRange = editRange; + } + + return completionList; + } + + /// + /// If the item doesn't define a TextEdit, then compute one based on the EditRange in the + /// + public static void AddEditRangeTextEdit(this CompletionItem item, CompletionRenderSettings settings, CompletionRenderContext ctx) + { + if (item.TextEdit is null) + { + item.TextEdit = new TextEdit { + NewText = item.TextEditText ?? item.InsertText ?? item.Label, + Range = ctx.EditRange + }; + } + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs b/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs new file mode 100644 index 00000000..d3f8d795 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/CompletionResolveHandler.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using System.Text.Json; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +using Roslyn.LanguageServer.Protocol; + +using CompletionResolveData = Microsoft.CodeAnalysis.LanguageServer.Handler.Completion.CompletionResolveData; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +[ExportCSharpVisualBasicStatelessLspService(typeof(CompletionResolveHandler)), Shared] +[Method(Methods.TextDocumentCompletionResolveName)] +sealed class CompletionResolveHandler : ILspServiceRequestHandler +{ + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => false; + + public async Task HandleRequestAsync(CompletionItem request, RequestContext context, CancellationToken cancellationToken) + { + if(request.Data is not JsonElement data) + { + throw new InvalidOperationException("Completion item is missing data"); + } + var resolveData = data.Deserialize(ProtocolConversions.LspJsonSerializerOptions); + if(resolveData?.ResultId == null) + { + throw new InvalidOperationException("Completion item is missing resultId"); + } + + var completionListCache = context.GetRequiredService(); + + var cacheEntry = completionListCache.GetCachedEntry(resolveData.ResultId); + if(cacheEntry == null) + { + throw new InvalidOperationException("Did not find cached completion list"); + } + + var clientCapabilities = context.GetRequiredClientCapabilities(); + var completionCapabilities = CompletionClientCapabilities.Create(clientCapabilities); + var renderSettings = new CompletionRenderSettings(completionCapabilities, true); + + foreach(var item in cacheEntry.Items) + { + if(item.IsMatch(request)) + { + var renderedItem = await item.Render(renderSettings, cacheEntry.Context, cancellationToken).ConfigureAwait(false); + + // avoid ambiguity if there are multiple items with the same label and IsMatch didn't distinguish + if(!string.Equals(renderedItem.SortText, request.SortText) || !string.Equals(renderedItem.FilterText, request.FilterText)) + { + continue; + } + + // if the client doesn't support EditRange, and the item doesn't define a TextEdit, we must add a computed one + if(!renderSettings.SupportsEditRange) + { + renderedItem.AddEditRangeTextEdit(renderSettings, cacheEntry.Context); + } + return renderedItem; + }; + } + + throw new InvalidOperationException("Did not find completion item in cached list"); + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs b/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs new file mode 100644 index 00000000..59fc47c7 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/ILspCompletionItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// An object that can be resolved to a completion item +/// +interface ILspCompletionItem +{ + /// + /// Resolve this object to a + /// + /// Provides information about which properties should be included + /// A populated + ValueTask Render(CompletionRenderSettings settings, CompletionRenderContext ctx, CancellationToken cancellationToken); + + /// + /// Whether this is a match for resolving the requested item. + /// + bool IsMatch(CompletionItem request); +} + +/// +/// Information common to rendering many/all items that may be used when rendering the +/// items upfront or cached and provided later when the item is resolved. +/// +/// +record struct CompletionRenderContext(LSP.Range EditRange); \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs new file mode 100644 index 00000000..910a7caa --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionDocsProvider.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; +using MonoDevelop.MSBuild.Language; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; +using MonoDevelop.MSBuild.PackageSearch; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +class MSBuildCompletionDocsProvider(DisplayElementRenderer renderer, MSBuildRootDocument document, SourceText sourceText, MSBuildResolveResult resolveResult) +{ + internal static MSBuildCompletionDocsProvider Create(ILogger logger, ClientCapabilities clientCapabilities, ClientInfo? clientInfo, MSBuildRootDocument document, SourceText sourceText, MSBuildResolveResult rr) + { + var documentationFormat = clientCapabilities.TextDocument?.Completion?.CompletionItem?.DocumentationFormat; + var renderer = new DisplayElementRenderer(logger, clientCapabilities, clientInfo, documentationFormat); + return new MSBuildCompletionDocsProvider(renderer, document, sourceText, rr); + } + + public async Task GetDocumentation(ISymbol symbol, CancellationToken cancellationToken) + { + var tooltipContent = await renderer.GetInfoTooltipElement(sourceText, document, symbol, resolveResult, false, cancellationToken); + if(tooltipContent is not null) + { + return new MarkupContent { + //Value = "$(symbol-keyword) keyword Choose\r\n\r\nGroups When and Otherwise elements", // tooltipContent, + Value = tooltipContent, + Kind = MarkupKind.Markdown + }; + } + return null; + } + + public async Task GetPackageDocumentation(IPackageSearchManager packageSearchManager, string packageId, string? packageVersion, FeedKind feedKind, string? targetFrameworkSearchParameter, CancellationToken cancellationToken) + { + var packageInfos = await packageSearchManager.SearchPackageInfo(packageId, null, targetFrameworkSearchParameter).ToTask(cancellationToken); + var packageInfo = packageInfos.FirstOrDefault(p => p.SourceKind == feedKind) ?? packageInfos.FirstOrDefault(); + + var tooltipContent = renderer.GetPackageInfoTooltip(packageId, packageInfo); + if(tooltipContent is not null) + { + return new MarkupContent { + Value = tooltipContent, + Kind = MarkupKind.Markdown + }; + } + return null; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs new file mode 100644 index 00000000..2cd6ce96 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/MSBuildCompletionItemKind.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Typesystem; + +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// Maps MSBuild completion items to LSP +/// +static class MSBuildCompletionItemKind +{ + public const CompletionItemKind Property = CompletionItemKind.Property; + public const CompletionItemKind Item = CompletionItemKind.Class; + public const CompletionItemKind Metadata = CompletionItemKind.Property; + public const CompletionItemKind Function = CompletionItemKind.Function; + + public const CompletionItemKind Constant = CompletionItemKind.Constant; + + public const CompletionItemKind PropertySyntax = CompletionItemKind.Property; + public const CompletionItemKind ItemSyntax = CompletionItemKind.Class; + public const CompletionItemKind MetadataSyntax = CompletionItemKind.Property; + + public const CompletionItemKind Culture = CompletionItemKind.Constant; + public const CompletionItemKind Sdk = CompletionItemKind.Module; + + public const CompletionItemKind NewGuid = CompletionItemKind.Macro; + + // TODO: icons + public const CompletionItemKind PackageNuGet = CompletionItemKind.Module; + public const CompletionItemKind PackageMyGet = CompletionItemKind.Module; + public const CompletionItemKind PackageLocal = CompletionItemKind.Module; + + internal static CompletionItemKind GetCompletionItemKind(this ISymbol symbol) + => symbol switch { + PropertyInfo => Property, + ItemInfo => Item, + MetadataInfo => Metadata, + ConstantSymbol => Constant, + _ => CompletionItemKind.Element + }; + + internal static CompletionItemKind GetCompletionItemKind(this FeedKind feedKind) + => feedKind switch { + FeedKind.MyGet => PackageMyGet, + FeedKind.Local => PackageLocal, + _ => PackageNuGet + }; +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs b/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs new file mode 100644 index 00000000..6a84b15c --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/MSBuildXmlCompletionDataSource.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.Text; + +using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Syntax; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Editor.Completion; +using MonoDevelop.Xml.Parser; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +record class MSBuildXmlCompletionContext + ( + XmlSpineParser SpineParser, XmlCompletionTrigger XmlTriggerKind, ITextSource TextSource, List NodePath, LSP.Range EditRange, + MSBuildResolveResult ResolveResult, MSBuildRootDocument Document, MSBuildCompletionDocsProvider DocsProvider, SourceText SourceText + ) + : XmlCompletionContext + ( + SpineParser, XmlTriggerKind, TextSource, NodePath, EditRange + ) +{ +} + +class MSBuildXmlCompletionDataSource : XmlCompletionDataSource +{ + protected override Task?> GetElementCompletionsAsync(MSBuildXmlCompletionContext context, bool includeBracket, CancellationToken token) + { + var doc = context.Document; + + var nodePath = context.NodePath; + if (!CompletionHelpers.TryGetElementSyntaxForElementCompletion(nodePath, out MSBuildElementSyntax? languageElement, out string? elementName)) { + return TaskCompleted(null); + } + + var items = new List(); + + foreach(var el in doc.GetElementCompletions(languageElement, elementName)) + { + if(el is ItemInfo) + { + items.Add(new MSBuildCompletionItem(el, XmlCommitKind.SelfClosingElement, context.DocsProvider, includeBracket ? "<" : null)); + } else + { + items.Add(new MSBuildCompletionItem(el, XmlCommitKind.Element, context.DocsProvider, includeBracket ? "<" : null)); + } + } + + bool allowCData = languageElement != null && languageElement.ValueKind != MSBuildValueKind.Nothing; + + return TaskCompleted(items); + } + + protected override Task?> GetAttributeCompletionsAsync(MSBuildXmlCompletionContext context, IAttributedXObject attributedObject, Dictionary existingAttributes, CancellationToken token) + { + var rr = context.ResolveResult; + var doc = context.Document; + + if(rr?.ElementSyntax == null) + { + return TaskCompleted(null); + } + + var items = new List(); + + foreach(var att in rr.GetAttributeCompletions(doc, doc.ToolsVersion)) + { + if(!existingAttributes.ContainsKey(att.Name)) + { + items.Add(new MSBuildCompletionItem(att, XmlCommitKind.Attribute, context.DocsProvider)); + } + } + + return TaskCompleted(items); + } + + protected override bool AllowTextContentInElement(MSBuildXmlCompletionContext context) + { + // when completing a tag name this is used to determine whether to include CDATA + // so we need to base it off the same MSBuildElementSyntax used for completion + // TODO: eliminate the duplicate TryGetElementSyntaxForElementCompletion call + if(context.XmlTriggerKind == XmlCompletionTrigger.ElementName || context.XmlTriggerKind == XmlCompletionTrigger.Tag) + { + var nodePath = context.NodePath; + if(!CompletionHelpers.TryGetElementSyntaxForElementCompletion(nodePath, out MSBuildElementSyntax? languageElement, out _)) + { + return true; + } + return languageElement is null || languageElement.ValueKind != MSBuildValueKind.Nothing; + } + + // otherwise, it's for entity completion, and the resolveResult is fine + if(context.ResolveResult?.ElementSyntax is { } elementSyntax) + { + return elementSyntax.ValueKind != MSBuildValueKind.Nothing; + } + + return true; + } +} diff --git a/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs b/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs new file mode 100644 index 00000000..dba94892 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/PackageCompletionDocsProvider.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +record class PackageCompletionDocsProvider(IPackageSearchManager PackageSearchManager, MSBuildCompletionDocsProvider docsProvider, string? TargetFrameworkSearchParameter) +{ + public Task GetPackageDocumentation(string packageId, string? packageVersion, FeedKind feedKind, CancellationToken cancellationToken) + => docsProvider.GetPackageDocumentation(PackageSearchManager, packageId, packageVersion, feedKind, TargetFrameworkSearchParameter, cancellationToken); +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs b/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs new file mode 100644 index 00000000..fc4cecd1 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/XmlCommitKind.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// Controls how XML completion items are committed +/// +enum XmlCommitKind +{ + Element, + SelfClosingElement, + Attribute, + AttributeValue, + CData, + Comment, + Prolog, + Entity, + ClosingTag, + MultipleClosingTags +} diff --git a/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs b/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs new file mode 100644 index 00000000..2be86711 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/XmlCompletionDataSource.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion.CompletionItems; +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Editor.Completion; +using MonoDevelop.Xml.Parser; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +record class XmlCompletionContext(XmlSpineParser SpineParser, XmlCompletionTrigger XmlTriggerKind, ITextSource TextSource, List NodePath, LSP.Range EditRange) +{ +} + +class XmlCompletionDataSource where TContext : XmlCompletionContext +{ + public IEnumerable?>> GetCompletionTasks(TContext triggerContext, CancellationToken cancellationToken) + { + switch(triggerContext.XmlTriggerKind) + { + case XmlCompletionTrigger.ElementValue: + yield return GetElementValueCompletionsAsync(triggerContext, cancellationToken); + goto case XmlCompletionTrigger.Tag; + + case XmlCompletionTrigger.Tag: + case XmlCompletionTrigger.ElementName: + //TODO: if it's on the first or second line and there's no DTD declaration, add the DTDs, or at least (maxDepth: 1) is not IAttributedXObject attributedOb) + { + throw new InvalidOperationException("Did not find IAttributedXObject in stack for XmlCompletionTrigger.Attribute"); + } + triggerContext.SpineParser.Clone().AdvanceUntilEnded((XObject)attributedOb, triggerContext.TextSource, 1000); + var attributes = attributedOb.Attributes.ToDictionary(StringComparer.OrdinalIgnoreCase); + yield return GetAttributeCompletionsAsync(triggerContext, attributedOb, attributes, cancellationToken); + break; + + case XmlCompletionTrigger.AttributeValue: + if(triggerContext.SpineParser.Spine.TryPeek(out XAttribute? att) && triggerContext.SpineParser.Spine.TryPeek(1, out IAttributedXObject? attributedObject)) + { + yield return GetAttributeValueCompletionsAsync(triggerContext, attributedObject, att, cancellationToken); + } + break; + + case XmlCompletionTrigger.Entity: + bool isElement = triggerContext.NodePath.Count > 0 && triggerContext.NodePath[^1] is XElement; + if(!isElement || AllowTextContentInElement(triggerContext)) + { + yield return GetEntityCompletionsAsync(triggerContext, cancellationToken); + } + break; + + case XmlCompletionTrigger.DocType: + case XmlCompletionTrigger.DeclarationOrCDataOrComment: + yield return GetDeclarationCompletionsAsync(triggerContext, cancellationToken); + break; + } + } + + protected virtual Task?> GetElementCompletionsAsync(TContext context, bool includeBracket, CancellationToken token) + => TaskCompleted(null); + + protected virtual Task?> GetElementValueCompletionsAsync(TContext context, CancellationToken token) + => TaskCompleted(null); + + protected virtual Task?> GetAttributeCompletionsAsync(TContext context, IAttributedXObject attributedObject, Dictionary existingAttributes, CancellationToken token) + => TaskCompleted(null); + + protected virtual Task?> GetAttributeValueCompletionsAsync(TContext context, IAttributedXObject attributedObject, XAttribute attribute, CancellationToken token) + => TaskCompleted(null); + + protected virtual Task?> GetEntityCompletionsAsync(TContext context, CancellationToken token) + => TaskCompleted(entityItems); + + protected virtual Task?> GetDeclarationCompletionsAsync(TContext context, CancellationToken token) + => TaskCompleted( + AllowTextContentInElement(context) + ? [cdataItemWithBracket, commentItemWithBracket] + : [commentItemWithBracket] + ); + + protected virtual bool AllowTextContentInElement(TContext context) => true; + + protected static Task?> TaskCompleted(IList? items) => Task.FromResult(items); + + Task?> GetMiscellaneousTagsAsync(TContext triggerContext, bool includeBracket, CancellationToken cancellationToken) + => Task.Run(() => (IList?)GetMiscellaneousTags(triggerContext, includeBracket).ToList(), cancellationToken); + + /// + /// Gets completion items for closing tags, comments, CDATA etc. + /// + IEnumerable GetMiscellaneousTags(TContext context, bool includeBracket) + { + if(context.NodePath.Count == 0 & context.EditRange.Start.Line == 0) + { + yield return includeBracket ? prologItemWithBracket : prologItem; + } + + if(AllowTextContentInElement(context)) + { + yield return includeBracket ? cdataItemWithBracket : cdataItem; + } + + yield return includeBracket ? commentItemWithBracket : commentItem; + + foreach(var closingTag in GetClosingTags(context.NodePath, includeBracket)) + { + yield return closingTag; + } + } + + IEnumerable GetClosingTags(List nodePath, bool includeBracket) + { + var dedup = new HashSet(); + + //FIXME: search forward to see if tag's closed already + for(int i = nodePath.Count - 1; i >= 0; i--) + { + var ob = nodePath[i]; + if(!(ob is XElement el)) + continue; + if(!el.IsNamed || el.IsClosed) + yield break; + + string name = el.Name.FullName!; + if(!dedup.Add(name)) + { + continue; + } + + yield return new XmlClosingTagCompletionItem(includeBracket, name, el, dedup.Count); + } + } + + readonly XmlCompletionItem cdataItem = new("![CDATA[", XmlToLspCompletionItemKind.CData, "XML character data", XmlCommitKind.CData); + readonly XmlCompletionItem cdataItemWithBracket = new("" + readonly XmlCompletionItem prologItem = new("?xml", XmlToLspCompletionItemKind.Prolog, "XML prolog", XmlCommitKind.Prolog); + readonly XmlCompletionItem prologItemWithBracket = new(""), + new ("amp", "&"), + ]; +} diff --git a/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs b/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs new file mode 100644 index 00000000..a80cc3b4 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Completion/XmlToLspCompletionItemKind.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +/// +/// Central location for mapping XML item kinds to values +/// +class XmlToLspCompletionItemKind +{ + public const CompletionItemKind ClosingTag = CompletionItemKind.CloseElement; + public const CompletionItemKind Comment = CompletionItemKind.TagHelper; + public const CompletionItemKind CData = CompletionItemKind.TagHelper; + public const CompletionItemKind Prolog = CompletionItemKind.TagHelper; + public const CompletionItemKind Entity = CompletionItemKind.TagHelper; +} diff --git a/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs b/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs new file mode 100644 index 00000000..914c97fe --- /dev/null +++ b/MSBuildLanguageServer/Handler/DocumentDiagnosticsHandler.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.Xml.Analysis; + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler; + + +[ExportCSharpVisualBasicStatelessLspService(typeof(DocumentDiagnosticsHandler)), Shared] +[Method(Methods.TextDocumentDiagnosticName)] +sealed class DocumentDiagnosticsHandler : ILspServiceDocumentRequestHandler?> +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams request) => request.TextDocument; + + public async Task?> HandleRequestAsync(DocumentDiagnosticParams request, RequestContext context, CancellationToken cancellationToken) + { + var msbuildParserService = context.GetRequiredService(); + var xmlParserService = context.GetRequiredService(); + var logger = context.GetRequiredService(); + var extLogger = logger.ToILogger(); + + var document = context.GetRequiredDocument(); + + if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken)) + { + return null; + } + + var result = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true) + + + var msbuildDoc = result.MSBuildDocument; + var sourceText = result.XmlParseResult.Text; + + IEnumerable? msbuildDiagnostics = msbuildDoc.Diagnostics; + + if(msbuildDoc.ProjectElement is not null) + { + try + { + // FIXME move this to a service + var analyzerDriver = new MSBuildAnalyzerDriver(context.GetRequiredService ().ToILogger ()); + analyzerDriver.AddBuiltInAnalyzers(); + msbuildDiagnostics = await Task.Run(() => analyzerDriver.Analyze(msbuildDoc, true, cancellationToken), cancellationToken); + } catch(Exception ex) + { + logger.LogException(ex, "Error in analyzer service"); + } + } + + var convertedXmlDiagnostics = result.XmlParseResult.Diagnostics?.Select(d => ConvertDiagnostic(d, sourceText)) ?? []; + var convertedMSBuildDiagnostics = msbuildDiagnostics.Select(d => ConvertDiagnostic(d, sourceText)); + + return new FullDocumentDiagnosticReport { + Items = convertedMSBuildDiagnostics.Concat(convertedXmlDiagnostics).ToArray() + }; + } + + const string sourceName = "msbuild"; + + static Diagnostic ConvertDiagnostic(XmlDiagnostic xmlDiagnostic, SourceText sourceText) + { + return new Diagnostic { + Range = xmlDiagnostic.Span.ToLspRange(sourceText), + Message = xmlDiagnostic.GetFormattedMessageWithTitle(), + Severity = ConvertSeverity(xmlDiagnostic.Descriptor.Severity), + Source = sourceName + // don't use ID and code, it's generally too long and not useful + // Code = xmlDiagnostic.Descriptor.Id, + }; + } + + static DiagnosticSeverity ConvertSeverity(XmlDiagnosticSeverity severity) => severity switch { + XmlDiagnosticSeverity.Suggestion => DiagnosticSeverity.Information, + XmlDiagnosticSeverity.Warning => DiagnosticSeverity.Warning, + XmlDiagnosticSeverity.Error => DiagnosticSeverity.Error, + _ => throw new ArgumentException($"Unsupported XmlDiagnosticSeverity '{0}'") + }; + + static Diagnostic ConvertDiagnostic(MSBuildDiagnostic msbuildDiagnostic, SourceText sourceText) + { + DiagnosticTag[]? diagnosticTags = msbuildDiagnostic.Descriptor.Id switch { + CoreDiagnostics.DeprecatedWithMessage_Id => [DiagnosticTag.Deprecated], + CoreDiagnostics.RedundantMinimumVersion_Id => [DiagnosticTag.Unnecessary], + _ => null + }; + + return new Diagnostic { + Range = msbuildDiagnostic.Span.ToLspRange(sourceText), + Message = msbuildDiagnostic.GetFormattedMessageWithTitle(), + Severity = ConvertSeverity(msbuildDiagnostic.Descriptor.Severity), + Tags = diagnosticTags, + Source = sourceName + // don't use ID and code, it's generally too long and not useful + //Code = msbuildDiagnostic.Descriptor.Id, + }; + } + + static DiagnosticSeverity ConvertSeverity(MSBuildDiagnosticSeverity severity) => severity switch { + MSBuildDiagnosticSeverity.Suggestion => DiagnosticSeverity.Information, + MSBuildDiagnosticSeverity.Warning => DiagnosticSeverity.Warning, + MSBuildDiagnosticSeverity.Error => DiagnosticSeverity.Error, + _ => throw new ArgumentException($"Unsupported MSBuildDiagnosticSeverity '{0}'") + }; +} diff --git a/MSBuildLanguageServer/Handler/HoverHandler.cs b/MSBuildLanguageServer/Handler/HoverHandler.cs new file mode 100644 index 00000000..3849702e --- /dev/null +++ b/MSBuildLanguageServer/Handler/HoverHandler.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.NuGetSearch; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.MSBuild.PackageSearch; +using MonoDevelop.MSBuild.Schema; + +using ProjectFileTools.NuGetSearch.Contracts; + +using Roslyn.LanguageServer.Protocol; + +using Range = Roslyn.LanguageServer.Protocol.Range; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler; + +// partly based on MonoDevelop.MSBuild.Editor/QuickInfo/MSBuildQuickInfoSource.cs + +[ExportCSharpVisualBasicStatelessLspService(typeof(HoverHandler)), Shared] +[Method(Methods.TextDocumentHoverName)] +internal sealed class HoverHandler : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(TextDocumentPositionParams request) => request.TextDocument; + + public async Task HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken) + { + var logger = context.GetRequiredService(); + var extLogger = logger.ToILogger(); + + DisplayElementRenderer CreateRenderer() + { + var capabilities = context.GetRequiredClientCapabilities(); + var clientInfo = context.GetRequiredService().TryGetInitializeParams()?.ClientInfo; + return new DisplayElementRenderer(extLogger, capabilities, clientInfo, capabilities.TextDocument?.Hover?.ContentFormat); + } + + var msbuildParserService = context.GetRequiredService(); + var xmlParserService = context.GetRequiredService(); + + var document = context.GetRequiredDocument(); + + if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken)) + { + return null; + } + + var result = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true) + + if(result?.MSBuildDocument is not MSBuildRootDocument doc) + { + return null; + } + + var sourceText = result.XmlParseResult.Text; + var position = ProtocolConversions.PositionToLinePosition(request.Position); + int offset = position.ToOffset (sourceText); + + var spineParser = result.XmlParseResult.GetSpineParser(position, cancellationToken); + + var annotations = MSBuildNavigation.GetAnnotationsAtOffset(doc, offset)?.ToList(); + if(annotations != null && annotations.Count > 0) + { + // TODO navigation annotations + return CreateNavigationQuickInfo(CreateRenderer(), sourceText, annotations); + } + + var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider; + + //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards + await functionTypeProvider.EnsureInitialized(cancellationToken); + + var rr = MSBuildResolver.Resolve(spineParser, result.XmlParseResult.Text.GetTextSource(), result.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken); + if (rr is null) + { + return null; + } + + if(rr.ReferenceKind == MSBuildReferenceKind.NuGetID) + { + var packageSearchManager = context.GetRequiredLspService(); + return await CreateNuGetQuickInfo(CreateRenderer(), packageSearchManager, logger, sourceText, doc, rr, cancellationToken); + } + + var info = rr.GetResolvedReference(doc, functionTypeProvider); + if(info is null) { + return null; + } + + // don't include the deprecation message, as the validator should have added a warning that will be merged into this tooltip + var markdown = await CreateRenderer().GetInfoTooltipElement(sourceText, doc, info, rr, true, cancellationToken); + + if(markdown is not null) { + return CreateHover(rr.ToLspRange(sourceText), markdown); + } + return null; + } + + static Hover CreateHover(Range range, string markdown) + { + return new Hover { + Range = range, + Contents = new MarkupContent { + Kind = MarkupKind.Markdown, + Value = markdown + } + }; + } + + Hover CreateNavigationQuickInfo(DisplayElementRenderer renderer, SourceText sourceText, IEnumerable annotations) + { + var navs = annotations.ToList(); + var first = navs.First(); + var resolvedPathMarkdown = renderer.GetResolvedPathElement(navs); + + return CreateHover(first.Span.ToLspRange (sourceText), resolvedPathMarkdown); + } + + //FIXME: can we display some kind of "loading" message while it loads? + async Task CreateNuGetQuickInfo( + DisplayElementRenderer renderer, + IPackageSearchManager packageSearchManager, ILspLogger logger, + SourceText sourceText, MSBuildRootDocument doc, MSBuildResolveResult rr, CancellationToken token) + { + IPackageInfo? info = null; + var packageId = rr.GetNuGetIDReference(); + + try + { + var frameworkId = doc.GetTargetFrameworkNuGetSearchParameter(); + + //FIXME: can we use the correct version here? + var infos = await packageSearchManager.SearchPackageInfo(packageId, null, frameworkId).ToTask(token); + + + //prefer non-local results as they will have more metadata + info = infos + .FirstOrDefault(p => p.SourceKind != ProjectFileTools.NuGetSearch.Feeds.FeedKind.Local) + ?? infos.FirstOrDefault(); + } + catch(Exception ex) when(!(ex is OperationCanceledException && token.IsCancellationRequested)) + { + logger.LogException(ex); + } + + var markdown = renderer.GetPackageInfoTooltip(packageId, info); + return CreateHover(rr.ToLspRange (sourceText), markdown); + } + + class NullFunctionTypeProvider : IFunctionTypeProvider + { + public Task EnsureInitialized(CancellationToken token) => Task.CompletedTask; + public ClassInfo? GetClassInfo(string name) => null; + public IEnumerable GetClassNameCompletions() => []; + public ISymbol? GetEnumInfo(string reference) => null; + public FunctionInfo? GetItemFunctionInfo(string name) => null; + public IEnumerable GetItemFunctionNameCompletions() => []; + public FunctionInfo? GetPropertyFunctionInfo(MSBuildValueKind valueKind, string name) => null; + public IEnumerable GetPropertyFunctionNameCompletions(ExpressionNode triggerExpression) => []; + public FunctionInfo? GetStaticPropertyFunctionInfo(string className, string name) => null; + public MSBuildValueKind ResolveType(ExpressionPropertyNode node) => MSBuildValueKind.Unknown; + } +} diff --git a/MSBuildLanguageServer/Handler/LocationHelpers.cs b/MSBuildLanguageServer/Handler/LocationHelpers.cs new file mode 100644 index 00000000..ff80d807 --- /dev/null +++ b/MSBuildLanguageServer/Handler/LocationHelpers.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Text; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +using Roslyn.LanguageServer.Protocol; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler; + +static class LocationHelpers +{ + public static SumType? ConvertLocationLinksToLocationsIfNeeded(LocationLink[]? results, bool supportsLocationLink) + { + if(results is null || results.Length == 0) + { + return null; + } + + if(supportsLocationLink) + { + return results; + } + + if(results.Length == 1) + { + return LocationLinkToLocation(results[0]); + } + + return Array.ConvertAll(results, LocationLinkToLocation); + } + + public static Location LocationLinkToLocation(LocationLink ll) => new Location { + Uri = ll.TargetUri, + Range = ll.TargetRange + }; + + public static LSP.Range ConvertRangeViaWorkspace(LspEditorWorkspace workspace, string filePath, Xml.Dom.TextSpan? targetSpan) + { + if(targetSpan is null) + { + return EmptyRange; + } + + var uri = ProtocolConversions.CreateAbsoluteUri(filePath); + SourceText? sourceText = null; + if(workspace.GetTrackedLspText().TryGetValue(uri, out var tracked)) + { + sourceText = tracked.Text; + } else + { + sourceText = SourceText.From(filePath); + } + + return sourceText.GetLspRange(targetSpan.Value.Start, targetSpan.Value.Length); + } + + public static LocationLink CreateLocationLink(LSP.Range originRange, string targetPath, LSP.Range? targetRange = null, LSP.Range? targetSelectionRange = null) + { + targetRange ??= EmptyRange; + return new LocationLink { + OriginSelectionRange = originRange, + TargetUri = ProtocolConversions.CreateAbsoluteUri(targetPath), + TargetRange = targetRange, + TargetSelectionRange = targetSelectionRange ?? targetRange + }; + } + + public static readonly LSP.Range EmptyRange = new LSP.Range { + Start = new LSP.Position { Line = 0, Character = 0 }, + End = new LSP.Position { Line = 0, Character = 0 } + }; +} diff --git a/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs b/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs new file mode 100644 index 00000000..e925aee6 --- /dev/null +++ b/MSBuildLanguageServer/Handler/Navigation/FindAllReferencesHandler.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.Composition; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Services; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Language; + +using Roslyn.LanguageServer.Protocol; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Navigation; + +[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared] +[Method(Methods.TextDocumentReferencesName)] +sealed class FindAllReferencesHandler : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(ReferenceParams request) => request.TextDocument; + + public async Task HandleRequestAsync(ReferenceParams request, RequestContext context, CancellationToken cancellationToken) + { + var logger = context.GetRequiredService(); + var extLogger = logger.ToILogger(); + + var msbuildParserService = context.GetRequiredService(); + + var document = context.GetRequiredDocument(); + + if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken)) + { + return null; + } + + var parseResult = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true) + + if(parseResult?.MSBuildDocument is not MSBuildRootDocument doc) + { + return null; + } + + var sourceText = parseResult.XmlParseResult.Text; + var position = ProtocolConversions.PositionToLinePosition(request.Position); + int offset = position.ToOffset(sourceText); + + var spineParser = parseResult.XmlParseResult.GetSpineParser(position, cancellationToken); + + var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider; + + //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards + await functionTypeProvider.EnsureInitialized(cancellationToken); + + var rr = MSBuildResolver.Resolve(spineParser, parseResult.XmlParseResult.Text.GetTextSource(), parseResult.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken); + if(rr is null) + { + return null; + } + + if(!MSBuildReferenceCollector.CanCreate(rr)) + { + return null; + } + + var resultReporter = BufferedProgress.Create(request.PartialResultToken); + var navigationService = context.GetRequiredLspService(); + + await navigationService.FindReferences( + parseResult, + (doc, text, logger, reporter) => MSBuildReferenceCollector.Create(doc, text, logger, rr, functionTypeProvider, reporter), + resultReporter, + request.WorkDoneToken, + cancellationToken + ).ConfigureAwait(false); + + request.WorkDoneToken?.End(); + + return resultReporter.GetFlattenedValues(); + } +} diff --git a/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs b/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs new file mode 100644 index 00000000..be8882dd --- /dev/null +++ b/MSBuildLanguageServer/Handler/Navigation/GoToDefinitionHandler.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Services; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Editor.Navigation; +using MonoDevelop.MSBuild.Language; + +using Roslyn.LanguageServer.Protocol; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Navigation; + +[ExportCSharpVisualBasicStatelessLspService(typeof(GoToDefinitionHandler)), Shared] +[Method(Methods.TextDocumentDefinitionName)] +sealed class GoToDefinitionHandler : ILspServiceDocumentRequestHandler?> +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DefinitionParams request) => request.TextDocument; + + public async Task?> HandleRequestAsync(DefinitionParams request, RequestContext context, CancellationToken cancellationToken) + { + var logger = context.GetRequiredService(); + var extLogger = logger.ToILogger(); + + var msbuildParserService = context.GetRequiredService(); + + var document = context.GetRequiredDocument(); + + if(!msbuildParserService.TryGetParseResult(document.CurrentState, out Task? parseTask, cancellationToken)) + { + return null; + } + + var parseResult = await parseTask!; // not sure why we need the ! here, TryGetParseResult has NotNullWhen(true) + + if(parseResult?.MSBuildDocument is not MSBuildRootDocument doc) + { + return null; + } + + var sourceText = parseResult.XmlParseResult.Text; + var position = ProtocolConversions.PositionToLinePosition(request.Position); + int offset = position.ToOffset(sourceText); + + var spineParser = parseResult.XmlParseResult.GetSpineParser(position, cancellationToken); + + var functionTypeProvider = context.GetRequiredService().FunctionTypeProvider; + + //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards + await functionTypeProvider.EnsureInitialized(cancellationToken); + + var rr = MSBuildResolver.Resolve(spineParser, parseResult.XmlParseResult.Text.GetTextSource(), parseResult.MSBuildDocument, functionTypeProvider, extLogger, cancellationToken); + if(rr is null) + { + return null; + } + + var result = MSBuildNavigation.GetNavigation(doc, offset, rr); + if(result is null) + { + return null; + } + + var originRange = parseResult.XmlParseResult.Text.GetLspRange(result.Offset, result.Length); + + var locations = await GetGoToDefinitionLocations(result, parseResult, originRange, request, context, cancellationToken); + + var linkSupport = context.GetRequiredClientCapabilities().TextDocument?.Definition?.LinkSupport ?? false; + + return LocationHelpers.ConvertLocationLinksToLocationsIfNeeded(locations, linkSupport); + } + + static async Task GetGoToDefinitionLocations( + MSBuildNavigationResult result, + MSBuildParseResult originParseResult, + LSP.Range originRange, + DefinitionParams request, + RequestContext context, + CancellationToken cancellationToken) + { + if(result.Paths != null) + { + if(result.Paths.Length == 1) + { + return [LocationHelpers.CreateLocationLink(originRange, result.Paths[0])]; + } + if(result.Paths.Length > 1) + { + return result.Paths.Select(p => LocationHelpers.CreateLocationLink(originRange, p)).ToArray(); + } + } + + if(result.DestFile != null) + { + var workspace = context.GetRequiredLspService(); + var targetRange = LocationHelpers.ConvertRangeViaWorkspace(workspace, result.DestFile, result.TargetSpan); + return [LocationHelpers.CreateLocationLink(originRange, result.DestFile, targetRange)]; + } + + Func? resultFilter = null; + MSBuildReferenceCollectorFactory collectorFactory; + + switch(result.Kind) + { + case MSBuildReferenceKind.Target: + request.WorkDoneToken?.Begin($"Finding definitions for target '{result.Name}'"); + collectorFactory = (doc, text, logger, reporter) => new MSBuildTargetDefinitionCollector(doc, text, logger, result.Name!, reporter); + break; + case MSBuildReferenceKind.Item: + request.WorkDoneToken?.Begin($"Finding assignments for item '{result.Name}'"); + collectorFactory = (doc, text, logger, reporter) => new MSBuildItemReferenceCollector(doc, text, logger, result.Name!, reporter); + resultFilter = MSBuildNavigationHelpers.FilterUsageWrites; + break; + case MSBuildReferenceKind.Property: + request.WorkDoneToken?.Begin($"Finding assignments for property '{result.Name}'"); + collectorFactory = (doc, text, logger, reporter) => new MSBuildPropertyReferenceCollector(doc, text, logger, result.Name!, reporter); + resultFilter = MSBuildNavigationHelpers.FilterUsageWrites; + break; + case MSBuildReferenceKind.NuGetID: + // TODO: can we navigate to a package URL? + // OpenNuGetUrl(result.Name, EditorHost, logger); + default: + return null; + } + + var resultReporter = BufferedProgress.Create>(request.PartialResultToken, ll => new(ll)); + var findReferencesService = context.GetRequiredLspService(); + + await findReferencesService.FindReferences( + originParseResult, + originRange, + collectorFactory, + resultReporter, + request.WorkDoneToken, + cancellationToken, + resultFilter).ConfigureAwait(false); + + request.WorkDoneToken?.End(); + + return resultReporter.GetFlattenedValues(); + } +} diff --git a/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs b/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs new file mode 100644 index 00000000..66e85178 --- /dev/null +++ b/MSBuildLanguageServer/Handler/WorkDoneProgressExtensions.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Handler; + +static class WorkDoneProgressExtensions +{ + public static void Begin(this IProgress? progress, string title, bool? cancellable = null, string? message = null, int? percentage = null) + { + progress.Report(new WorkDoneProgressBegin { + Title = title, + Cancellable = cancellable, + Message = message, + Percentage = percentage + }); + } + + public static void Report(this IProgress progress, bool? cancellable = null, string? message = null, int? percentage = null) + { + progress.Report(new WorkDoneProgressReport{ + Cancellable = cancellable, + Message = message, + Percentage = percentage + }); + } + + public static void End(this IProgress? progress, string? message = null) + { + progress.Report(new WorkDoneProgressEnd{ + Message = message + }); + } +} + +/* +static class ProgressReportingExtensionMethods +{ + public static IProgress CreateProgress(this RequestContext context, TParams p) + where TParams : IPartialResultParams + where TResult : IPartialResult + { + var manager = context.GetRequiredLspService(); + return new ProgressReporter(manager, p.PartialResultToken, context.QueueCancellationToken); + } + + class ProgressReporter : IProgress + where TResult : IPartialResult + { + readonly IClientLanguageServerManager manager; + readonly ProgressToken partialResultToken; + readonly CancellationToken queueCancellationToken; + + public ProgressReporter(IClientLanguageServerManager manager, ProgressToken partialResultToken, CancellationToken queueCancellationToken) + { + this.manager = manager; + this.partialResultToken = partialResultToken; + this.queueCancellationToken = queueCancellationToken; + } + + public void Report(TResult value) + { + value.PartialResultToken = partialResultToken; + manager.SendNotificationAsync(Methods.ProgressNotificationName, queueCancellationToken); + } + } +} +*/ +/* + +[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared] +[Method(Methods.TextDocumentDocumentHighlightName)] +sealed class DocumentHighlightHandler : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentHighlightParams request) => request.TextDocument; + + public async Task HandleRequestAsync(DocumentHighlightParams request, RequestContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + +[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared] +[Method(Methods.TextDocumentDocumentLinkName)] +sealed class DocumentLinkHandler : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentLinkParams request) => request.TextDocument; + + public async Task HandleRequestAsync(DocumentLinkParams request, RequestContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + +[ExportCSharpVisualBasicStatelessLspService(typeof(FindAllReferencesHandler)), Shared] +[Method(Methods.DocumentLinkResolveName)] +sealed class DocumentLinkResolveHandler : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentLink request) => request.TextDocument; + + public async Task HandleRequestAsync(DocumentLink request, RequestContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + +*/ \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/ExportProviderBuilder.cs b/MSBuildLanguageServer/Import/ExportProviderBuilder.cs new file mode 100644 index 00000000..ce22a286 --- /dev/null +++ b/MSBuildLanguageServer/Import/ExportProviderBuilder.cs @@ -0,0 +1,120 @@ +// modified version of +// https://raw.githubusercontent.com/dotnet/roslyn/12f89683716864af2582b59f9b94395ad8f39910/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs +// changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +internal sealed class ExportProviderBuilder +{ + public static async Task CreateExportProviderAsync( + ExtensionAssemblyManager extensionManager, + IAssemblyLoader assemblyLoader, + string? devKitDependencyPath, + ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger(); + var baseDirectory = AppContext.BaseDirectory; + + // BEGIN MODIFICATION + /* + // Load any Roslyn assemblies from the extension directory + var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll"); + assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); + + // DevKit assemblies are not shipped in the main language server folder + // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC). + // So manually add them to the MEF catalog here. + if (devKitDependencyPath != null) + { + assemblyPaths = assemblyPaths.Concat(devKitDependencyPath); + } + */ + IEnumerable assemblyPaths = [ + typeof (global::MonoDevelop.MSBuild.Editor.Common.ThisAssembly).Assembly.Location, + typeof (global::MSBuildLanguageServer.ThisAssembly).Assembly.Location + ]; + // END MODIFICATION + + // Add the extension assemblies to the MEF catalog. + assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths); + + logger.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}."); + + // Create a MEF resolver that can resolve assemblies in the extension contexts. + var resolver = new Resolver(assemblyLoader); + + var discovery = PartDiscovery.Combine( + resolver, + new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) + new AttributedPartDiscoveryV1(resolver)); + + // TODO - we should likely cache the catalog so we don't have to rebuild it every time. + var catalog = ComposableCatalog.Create(resolver) + .AddParts(await discovery.CreatePartsAsync(assemblyPaths)) + .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import + + // Assemble the parts into a valid graph. + var config = CompositionConfiguration.Create(catalog); + + // Verify we only have expected errors. + ThrowOnUnexpectedErrors(config, logger); + + // Prepare an ExportProvider factory based on this graph. + var exportProviderFactory = config.CreateExportProviderFactory(); + + // Create an export provider, which represents a unique container of values. + // You can create as many of these as you want, but typically an app needs just one. + var exportProvider = exportProviderFactory.CreateExportProvider(); + + // Immediately set the logger factory, so that way it'll be available for the rest of the composition + exportProvider.GetExportedValue().SetFactory(loggerFactory); + + return exportProvider; + } + + private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ILogger logger) + { + // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + // Currently we are expecting the following: + // "----- CompositionError level 1 ------ + // Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider.ctor(implementation): expected exactly 1 export matching constraints: + // Contract name: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation + // TypeIdentityName: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation + // but found 0. + // part definition Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider + var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? Enumerable.Empty(); + + // BEGIN MODIFICATION + /* + var expectedErroredParts = new string[] { "PythiaSignatureHelpProvider" }; + */ + var expectedErroredParts = new string[] { }; + // END MODIFICATION + + if (erroredParts.Count() != expectedErroredParts.Length || !erroredParts.All(part => expectedErroredParts.Contains(part))) + { + try + { + configuration.ThrowOnErrors(); + } + catch (CompositionFailedException ex) + { + // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately here. + logger.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/Extensions.cs b/MSBuildLanguageServer/Import/Extensions.cs new file mode 100644 index 00000000..2fc6a024 --- /dev/null +++ b/MSBuildLanguageServer/Import/Extensions.cs @@ -0,0 +1,278 @@ +// based on +// https://raw.githubusercontent.com/dotnet/roslyn/b96c245a17de4fba336832b0d4f593c1960b376c/src/LanguageServer/Protocol/Extensions/Extensions.cs +// with some parts commented out with /* */ comments + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +/* +using Microsoft.CodeAnalysis.FindUsages; +*/ +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Text.Adornments; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer +{ + internal static class Extensions + { + /* + public static Uri GetURI(this TextDocument document) + { + Contract.ThrowIfNull(document.FilePath); + return document is SourceGeneratedDocument + ? ProtocolConversions.CreateUriFromSourceGeneratedFilePath(document.FilePath) + : ProtocolConversions.CreateAbsoluteUri(document.FilePath); + } + + /// + /// Generate the Uri of a document by replace the name in file path using the document's name. + /// Used to generate the correct Uri when rename a document, because calling doesn't update the file path. + /// + public static Uri GetUriForRenamedDocument(this TextDocument document) + { + Contract.ThrowIfNull(document.FilePath); + Contract.ThrowIfNull(document.Name); + Contract.ThrowIfTrue(document is SourceGeneratedDocument); + var directoryName = Path.GetDirectoryName(document.FilePath); + + Contract.ThrowIfNull(directoryName); + var path = Path.Combine(directoryName, document.Name); + return ProtocolConversions.CreateAbsoluteUri(path); + } + + public static Uri CreateUriForDocumentWithoutFilePath(this TextDocument document) + { + Contract.ThrowIfNull(document.Name); + Contract.ThrowIfNull(document.Project.FilePath); + + var projectDirectoryName = Path.GetDirectoryName(document.Project.FilePath); + Contract.ThrowIfNull(projectDirectoryName); + var path = Path.Combine([projectDirectoryName, .. document.Folders, document.Name]); + return ProtocolConversions.CreateAbsoluteUri(path); + } + + public static ImmutableArray GetDocuments(this Solution solution, Uri documentUri) + => GetDocuments(solution, ProtocolConversions.GetDocumentFilePathFromUri(documentUri)); + + public static ImmutableArray GetDocuments(this Solution solution, string documentPath) + { + var documentIds = solution.GetDocumentIdsWithFilePath(documentPath); + + // We don't call GetRequiredDocument here as the id could be referring to an additional document. + var documents = documentIds.Select(solution.GetDocument).WhereNotNull().ToImmutableArray(); + return documents; + } + + /// + /// Get all regular and additional s for the given . + /// + public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri) + { + var documentIds = GetDocumentIds(solution, documentUri); + + var documents = documentIds + .Select(solution.GetDocument) + .Concat(documentIds.Select(solution.GetAdditionalDocument)) + .WhereNotNull() + .ToImmutableArray(); + return documents; + } + + public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri) + => solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri)); + + public static Document? GetDocument(this Solution solution, TextDocumentIdentifier documentIdentifier) + { + var documents = solution.GetDocuments(documentIdentifier.Uri); + return documents.Length == 0 + ? null + : documents.FindDocumentInProjectContext(documentIdentifier, (sln, id) => sln.GetRequiredDocument(id)); + } + + private static T FindItemInProjectContext( + ImmutableArray items, + TextDocumentIdentifier itemIdentifier, + Func projectIdGetter, + Func defaultGetter) + { + if (items.Length > 1) + { + // We have more than one document; try to find the one that matches the right context + if (itemIdentifier is VSTextDocumentIdentifier vsDocumentIdentifier && vsDocumentIdentifier.ProjectContext != null) + { + var projectId = ProtocolConversions.ProjectContextToProjectId(vsDocumentIdentifier.ProjectContext); + var matchingItem = items.FirstOrDefault(d => projectIdGetter(d) == projectId); + + if (matchingItem != null) + { + return matchingItem; + } + } + else + { + return defaultGetter(); + } + } + + // We either have only one item or have multiple, but none of them matched our context. In the + // latter case, we'll just return the first one arbitrarily since this might just be some temporary mis-sync + // of client and server state. + return items[0]; + } + + public static T FindDocumentInProjectContext(this ImmutableArray documents, TextDocumentIdentifier documentIdentifier, Func documentGetter) where T : TextDocument + { + return FindItemInProjectContext(documents, documentIdentifier, projectIdGetter: (item) => item.Project.Id, defaultGetter: () => + { + // We were not passed a project context. This can happen when the LSP powered NavBar is not enabled. + // This branch should be removed when we're using the LSP based navbar in all scenarios. + + var solution = documents.First().Project.Solution; + // Lookup which of the linked documents is currently active in the workspace. + var documentIdInCurrentContext = solution.Workspace.GetDocumentIdInCurrentContext(documents.First().Id); + return documentGetter(solution, documentIdInCurrentContext); + }); + } + + public static Project? GetProject(this Solution solution, TextDocumentIdentifier projectIdentifier) + { + var projects = solution.Projects.Where(project => project.FilePath == projectIdentifier.Uri.LocalPath).ToImmutableArray(); + return !projects.Any() + ? null + : FindItemInProjectContext(projects, projectIdentifier, projectIdGetter: (item) => item.Id, defaultGetter: () => projects[0]); + } + + public static TextDocument? GetAdditionalDocument(this Solution solution, TextDocumentIdentifier documentIdentifier) + { + var documentIds = GetDocumentIds(solution, documentIdentifier.Uri); + + // We don't call GetRequiredAdditionalDocument as the id could be referring to a regular document. + var additionalDocuments = documentIds.Select(solution.GetAdditionalDocument).WhereNotNull().ToImmutableArray(); + return !additionalDocuments.Any() + ? null + : additionalDocuments.FindDocumentInProjectContext(documentIdentifier, (sln, id) => sln.GetRequiredAdditionalDocument(id)); + } + + public static async Task GetPositionFromLinePositionAsync(this TextDocument document, LinePosition linePosition, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + return text.Lines.GetPosition(linePosition); + } + + public static bool HasVisualStudioLspCapability(this ClientCapabilities? clientCapabilities) + { + if (clientCapabilities is VSInternalClientCapabilities vsClientCapabilities) + { + return vsClientCapabilities.SupportsVisualStudioExtensions; + } + + return false; + } + + public static bool HasCompletionListDataCapability(this ClientCapabilities clientCapabilities) + { + if (!TryGetVSCompletionListSetting(clientCapabilities, out var completionListSetting)) + { + return false; + } + + return completionListSetting.Data; + } + + public static bool HasCompletionListCommitCharactersCapability(this ClientCapabilities clientCapabilities) + { + if (!TryGetVSCompletionListSetting(clientCapabilities, out var completionListSetting)) + { + return false; + } + + return completionListSetting.CommitCharacters; + } + + public static string GetMarkdownLanguageName(this Document document) + { + switch (document.Project.Language) + { + case LanguageNames.CSharp: + return "csharp"; + case LanguageNames.VisualBasic: + return "vb"; + case LanguageNames.FSharp: + return "fsharp"; + case InternalLanguageNames.TypeScript: + return "typescript"; + default: + throw new ArgumentException(string.Format("Document project language {0} is not valid", document.Project.Language)); + } + } + + public static ClassifiedTextElement GetClassifiedText(this DefinitionItem definition) + => new ClassifiedTextElement(definition.DisplayParts.Select(part => new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text))); + + private static bool TryGetVSCompletionListSetting(ClientCapabilities clientCapabilities, [NotNullWhen(returnValue: true)] out VSInternalCompletionListSetting? completionListSetting) + { + if (clientCapabilities is not VSInternalClientCapabilities vsClientCapabilities) + { + completionListSetting = null; + return false; + } + + var textDocumentCapability = vsClientCapabilities.TextDocument; + if (textDocumentCapability == null) + { + completionListSetting = null; + return false; + } + + if (textDocumentCapability.Completion is not VSInternalCompletionSetting vsCompletionSetting) + { + completionListSetting = null; + return false; + } + + completionListSetting = vsCompletionSetting.CompletionList; + if (completionListSetting == null) + { + return false; + } + + return true; + } + */ + + public static int CompareTo(this Position p1, Position p2) + { + if (p1.Line > p2.Line) + return 1; + else if (p1.Line < p2.Line) + return -1; + + if (p1.Character > p2.Character) + return 1; + else if (p1.Character < p2.Character) + return -1; + + return 0; + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/LspWorkspaceManager.cs b/MSBuildLanguageServer/Import/LspWorkspaceManager.cs new file mode 100644 index 00000000..8175d89d --- /dev/null +++ b/MSBuildLanguageServer/Import/LspWorkspaceManager.cs @@ -0,0 +1,479 @@ +// based on +// https://raw.githubusercontent.com/dotnet/roslyn/dd1d6535773f71c1f921589cf59d1d62f6e8c27f/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +// with some parts commented out with /* */ comments + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +/// Manages the registered workspaces and corresponding LSP solutions for an LSP server. +/// This type is tied to a particular server. +/// +/// +/// This type provides an LSP view of the registered workspace solutions so that all LSP requests operate +/// on the state of the world that matches the LSP requests we've received. +/// +/// This is done by storing the LSP text as provided by client didOpen/didClose/didChange requests. When asked for a document we provide either +/// +/// The exact workspace solution instance if all the LSP text matches what is currently in the workspace. +/// A fork from the workspace current solution with the LSP text applied if the LSP text does not match. This can happen since +/// LSP text sync is asynchronous and not guaranteed to match the text in the workspace (though the majority of the time in VS it does). +/// +/// +/// Doing the forking like this has a few nice properties. +/// +/// 99% of the time the VS workspace matches the LSP text. In those cases we do 0 re-parsing, share compilations, versions, checksum calcs, etc. +/// In the 1% of the time that we do not match, we can simply and easily compute a fork. +/// The code is relatively straightforward +/// +/// +internal sealed class LspWorkspaceManager : IDocumentChangeTracker, ILspService +{ + /* + /// + /// A cache from workspace to the last solution we returned for LSP. + /// The forkedFromVersion is not null when the solution was created from a fork of the workspace with LSP + /// text applied on top. It is null when LSP reuses the workspace solution (the LSP text matches the contents of the + /// workspace). + /// Access to this is guaranteed to be serial by the + /// + private readonly Dictionary _cachedLspSolutions = []; + */ + /// + /// Stores the current source text for each URI that is being tracked by LSP. Each time an LSP text sync + /// notification comes in, this source text is updated to match. Used as the backing implementation for the . + /// Note that the text here is tracked regardless of whether or not we found a matching roslyn document for + /// the URI. + /// Access to this is guaranteed to be serial by the + /// + private ImmutableDictionary _trackedDocuments = ImmutableDictionary.Empty; + + private readonly ILspLogger _logger; + /* + private readonly LspMiscellaneousFilesWorkspace? _lspMiscellaneousFilesWorkspace; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; + private readonly ILanguageInfoProvider _languageInfoProvider; + */ + private readonly RequestTelemetryLogger _requestTelemetryLogger; + + public LspWorkspaceManager( + ILspLogger logger, + /* + LspMiscellaneousFilesWorkspace? lspMiscellaneousFilesWorkspace, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + ILanguageInfoProvider languageInfoProvider, + */ + RequestTelemetryLogger requestTelemetryLogger) + { + /* + _lspMiscellaneousFilesWorkspace = lspMiscellaneousFilesWorkspace; + */ + _logger = logger; + _requestTelemetryLogger = requestTelemetryLogger; + /* + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; + _languageInfoProvider = languageInfoProvider; + */ + } + + public EventHandler? LspTextChanged; + + #region Implementation of IDocumentChangeTracker + /* + private static async ValueTask ApplyChangeToMutatingWorkspaceAsync(Workspace workspace, Uri uri, Func change) + { + if (workspace is not ILspWorkspace { SupportsMutation: true } mutatingWorkspace) + return; + + foreach (var documentId in workspace.CurrentSolution.GetDocumentIds(uri)) + await change(mutatingWorkspace, documentId).ConfigureAwait(false); + } + */ + /// + /// Called by the when a document is opened in LSP. + /// + /// is true which means this runs serially in the + /// + public async ValueTask StartTrackingAsync(Uri uri, SourceText documentText, string languageId, CancellationToken cancellationToken) + { + // First, store the LSP view of the text as the uri is now owned by the LSP client. + Contract.ThrowIfTrue(_trackedDocuments.ContainsKey(uri), $"didOpen received for {uri} which is already open."); + _trackedDocuments = _trackedDocuments.Add(uri, (documentText, languageId)); + /* + // If LSP changed, we need to compare against the workspace again to get the updated solution. + _cachedLspSolutions.Clear(); + */ + LspTextChanged?.Invoke(this, EventArgs.Empty); + /* + // Attempt to open the doc if we find it in a workspace. Note: if we don't (because we've heard from lsp about + // the doc before we've heard from the project system), that's ok. We'll still attempt to open it later in + // GetLspSolutionForWorkspaceAsync + await TryOpenDocumentsInMutatingWorkspaceAsync(uri).ConfigureAwait(false); + + return; + + async ValueTask TryOpenDocumentsInMutatingWorkspaceAsync(Uri uri) + { + var registeredWorkspaces = _lspWorkspaceRegistrationService.GetAllRegistrations(); + foreach (var workspace in registeredWorkspaces) + { + await ApplyChangeToMutatingWorkspaceAsync(workspace, uri, (_, documentId) => + workspace.TryOnDocumentOpenedAsync(documentId, documentText.Container, isCurrentContext: false, cancellationToken)).ConfigureAwait(false); + } + } + */ + } + + /// + /// Called by the when a document is closed in LSP. + /// + /// is true which means this runs serially in the + /// + public async ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellationToken) + { + // First, stop tracking this URI and source text as it is no longer owned by LSP. + Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(uri), $"didClose received for {uri} which is not open."); + _trackedDocuments = _trackedDocuments.Remove(uri); + /* + // If LSP changed, we need to compare against the workspace again to get the updated solution. + _cachedLspSolutions.Clear(); + + // Also remove it from our loose files workspace if it is still there. + _lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri); + + LspTextChanged?.Invoke(this, EventArgs.Empty); + + // Attempt to close the doc, if it is currently open in a workspace. + await TryCloseDocumentsInMutatingWorkspaceAsync(uri).ConfigureAwait(false); + + return; + + async ValueTask TryCloseDocumentsInMutatingWorkspaceAsync(Uri uri) + { + var registeredWorkspaces = _lspWorkspaceRegistrationService.GetAllRegistrations(); + foreach (var workspace in registeredWorkspaces) + { + await ApplyChangeToMutatingWorkspaceAsync(workspace, uri, (_, documentId) => + workspace.TryOnDocumentClosedAsync(documentId, cancellationToken)).ConfigureAwait(false); + } + } + */ + } + + /// + /// Called by the when a document's text is updated in LSP. + /// + /// is true which means this runs serially in the + /// + public void UpdateTrackedDocument(Uri uri, SourceText newSourceText) + { + // Store the updated LSP view of the source text. + Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(uri), $"didChange received for {uri} which is not open."); + var (_, language) = _trackedDocuments[uri]; + _trackedDocuments = _trackedDocuments.SetItem(uri, (newSourceText, language)); + + /* + // If LSP changed, we need to compare against the workspace again to get the updated solution. + _cachedLspSolutions.Clear(); + */ + LspTextChanged?.Invoke(this, EventArgs.Empty); + } + + public ImmutableDictionary GetTrackedLspText() => _trackedDocuments; + + #endregion + /* + #region LSP Solution Retrieval + + /// + /// Returns the LSP solution associated with the workspace with workspace kind . + /// This is the solution used for LSP requests that pertain to the entire workspace, for example code search or + /// workspace diagnostics. + /// + /// This is always called serially in the when creating the . + /// + public async Task<(Workspace?, Solution?)> GetLspSolutionInfoAsync(CancellationToken cancellationToken) + { + // Ensure we have the latest lsp solutions + var updatedSolutions = await GetLspSolutionsAsync(cancellationToken).ConfigureAwait(false); + + var (hostWorkspace, hostWorkspaceSolution, isForked) = updatedSolutions.FirstOrDefault(lspSolution => lspSolution.Solution.WorkspaceKind is WorkspaceKind.Host); + _requestTelemetryLogger.UpdateUsedForkedSolutionCounter(isForked); + + return (hostWorkspace, hostWorkspaceSolution); + } + + /// + /// Returns the LSP solution associated with the workspace with kind . This is the + /// solution used for LSP requests that pertain to the entire workspace, for example code search or workspace + /// diagnostics. + /// + /// This is always called serially in the when creating the . + /// + public async Task<(Workspace?, Solution?, TextDocument?)> GetLspDocumentInfoAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken) + { + // Get the LSP view of all the workspace solutions. + var uri = textDocumentIdentifier.Uri; + var lspSolutions = await GetLspSolutionsAsync(cancellationToken).ConfigureAwait(false); + + // Find the matching document from the LSP solutions. + foreach (var (workspace, lspSolution, isForked) in lspSolutions) + { + var documents = lspSolution.GetTextDocuments(textDocumentIdentifier.Uri); + if (documents.Any()) + { + var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); + + // Record metadata on how we got this document. + var workspaceKind = document.Project.Solution.WorkspaceKind; + _requestTelemetryLogger.UpdateFindDocumentTelemetryData(success: true, workspaceKind); + _requestTelemetryLogger.UpdateUsedForkedSolutionCounter(isForked); + _logger.LogInformation($"{document.FilePath} found in workspace {workspaceKind}"); + + // As we found the document in a non-misc workspace, also attempt to remove it from the misc workspace + // if it happens to be in there as well. + if (workspace != _lspMiscellaneousFilesWorkspace) + _lspMiscellaneousFilesWorkspace?.TryRemoveMiscellaneousDocument(uri); + + return (workspace, document.Project.Solution, document); + } + } + + // We didn't find the document in any workspace, record a telemetry notification that we did not find it. + // Depending on the host, this can be entirely normal (e.g. opening a loose file) + var searchedWorkspaceKinds = string.Join(";", lspSolutions.SelectAsArray(lspSolution => lspSolution.Solution.Workspace.Kind)); + _logger.LogInformation($"Could not find '{textDocumentIdentifier.Uri}'. Searched {searchedWorkspaceKinds}"); + _requestTelemetryLogger.UpdateFindDocumentTelemetryData(success: false, workspaceKind: null); + + // Add the document to our loose files workspace (if we have one) if it iss open. + if (_trackedDocuments.TryGetValue(uri, out var trackedDocument)) + { + var miscDocument = _lspMiscellaneousFilesWorkspace?.AddMiscellaneousDocument(uri, trackedDocument.Text, trackedDocument.LanguageId, _logger); + if (miscDocument is not null) + return (_lspMiscellaneousFilesWorkspace, miscDocument.Project.Solution, miscDocument); + } + + return default; + } + + /// + /// Gets the LSP view of all the registered workspaces' current solutions. + /// + private async Task> GetLspSolutionsAsync(CancellationToken cancellationToken) + { + // Ensure that the loose files workspace is searched last. + var registeredWorkspaces = _lspWorkspaceRegistrationService.GetAllRegistrations(); + registeredWorkspaces = registeredWorkspaces + .Where(workspace => workspace.Kind != WorkspaceKind.MiscellaneousFiles) + .Concat(registeredWorkspaces.Where(workspace => workspace.Kind == WorkspaceKind.MiscellaneousFiles)) + .ToImmutableArray(); + + var solutions = new FixedSizeArrayBuilder<(Workspace, Solution, bool)>(registeredWorkspaces.Length); + foreach (var workspace in registeredWorkspaces) + { + // Retrieve the workspace's current view of the world at the time the request comes in. If this is changing + // underneath, it is either the job of the LSP client to poll us (diagnostics) or we send refresh + // notifications (semantic tokens) to the client letting them know that our workspace has changed and they + // need to re-query us. + var (lspSolution, isForked) = await GetLspSolutionForWorkspaceAsync(workspace, cancellationToken).ConfigureAwait(false); + solutions.Add((workspace, lspSolution, isForked)); + } + + return solutions.MoveToImmutable(); + + async Task<(Solution Solution, bool IsForked)> GetLspSolutionForWorkspaceAsync(Workspace workspace, CancellationToken cancellationToken) + { + var workspaceCurrentSolution = workspace.CurrentSolution; + + // At a high level these are the steps we take to compute what the desired LSP solution should be. + // + // 1. First we want to check if our workspace current solution is the same as the last workspace current + // solution that we verified matches the LSP text. If so, we can skip comparing the LSP text against the + // workspace text and just return the cached one since absolutely nothing has changed. Importantly, we + // do not return a cached forked solution - we do not want to re-use a forked solution if the LSP text + // has changed and now matches the workspace. + // + // 2. Next, ensure that any changes we've collected are pushed through to the underlying workspace *if* + // it's a mutating workspace. This will bring that workspace into sync with all that we've heard from lsp. + // + // 3. If the cached solution isn't a match, we compare the LSP text to the workspace's text and return the + // workspace text if all LSP text matches. While this does compute checksums, generally speaking that's + // a reasonable price to pay. For example, we always do this in VS anyways to make OOP calls, and it is + // not a burden there. + // + // 4. Third, we check to see if we have cached a forked LSP solution for the current set of LSP texts + // against the current workspace version. If so, we can just reuse that instead of re-forking and + // blowing away the trees / source generated docs / etc. that we created for the fork. + // + // 5. We have nothing cached for this combination of LSP texts and workspace version. We have exhausted + // our options and must create an LSP fork from the current workspace solution with the current LSP + // text. + // + // We propagate the IsForked value back up so that we only report telemetry on forking if the forked + // solution is actually requested. + + // Step 1: Check if nothing has changed and we already verified that the workspace text matches our LSP text. + if (_cachedLspSolutions.TryGetValue(workspace, out var cachedSolution) && cachedSolution.solution == workspaceCurrentSolution) + return (workspaceCurrentSolution, IsForked: false); + + // Step 2: Push through any changes to the underlying workspace if it's a mutating workspace. + await TryOpenAndEditDocumentsInMutatingWorkspaceAsync(workspace).ConfigureAwait(false); + + // Because the workspace may have been mutated, go back and retrieve its current snapshot so we're operating + // against that view. + workspaceCurrentSolution = workspace.CurrentSolution; + + // Step 3: Check to see if the LSP text matches the workspace text. + var documentsInWorkspace = GetDocumentsForUris(_trackedDocuments.Keys.ToImmutableArray(), workspaceCurrentSolution); + if (await DoesAllTextMatchWorkspaceSolutionAsync(documentsInWorkspace, cancellationToken).ConfigureAwait(false)) + { + // Remember that the current LSP text matches the text in this workspace solution. + _cachedLspSolutions[workspace] = (forkedFromVersion: null, workspaceCurrentSolution); + return (workspaceCurrentSolution, IsForked: false); + } + + // Step 4: See if we can reuse a previously forked solution. + if (cachedSolution != default && cachedSolution.forkedFromVersion == workspaceCurrentSolution.WorkspaceVersion) + return (cachedSolution.solution, IsForked: true); + + // Step 5: Fork a new solution from the workspace with the LSP text applied. + var lspSolution = workspaceCurrentSolution; + foreach (var (uri, workspaceDocuments) in documentsInWorkspace) + lspSolution = lspSolution.WithDocumentText(workspaceDocuments.Select(d => d.Id), _trackedDocuments[uri].Text); + + // Remember this forked solution and the workspace version it was forked from. + _cachedLspSolutions[workspace] = (workspaceCurrentSolution.WorkspaceVersion, lspSolution); + return (lspSolution, IsForked: true); + } + + async ValueTask TryOpenAndEditDocumentsInMutatingWorkspaceAsync(Workspace workspace) + { + foreach (var (uri, (sourceText, _)) in _trackedDocuments) + { + await ApplyChangeToMutatingWorkspaceAsync(workspace, uri, async (mutatingWorkspace, documentId) => + { + // This may be the first time this workspace is hearing that this document is open from LSP's + // perspective. Attempt to open it there. + // + // TODO(cyrusn): Do we need to pass a correct value for isCurrentContext? Or will that fall out from + // something else in lsp. + await workspace.TryOnDocumentOpenedAsync( + documentId, sourceText.Container, isCurrentContext: false, cancellationToken).ConfigureAwait(false); + + // Note: there is a race here in that we might see/change/return here based on the + // relationship of 'sourceText' and 'currentSolution' while some other entity outside of the + // confines of lsp queue might update the workspace externally. That's completely fine + // though. The caller will always grab the 'current solution' again off of the workspace + // and check the checksums of all documents against the ones this workspace manager is + // tracking. If there are any differences, it will fork and use that fork. + await mutatingWorkspace.UpdateTextIfPresentAsync(documentId, sourceText, cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } + } + } + + /// + /// Given a set of documents from the workspace current solution, verify that the LSP text is the same as the document contents. + /// + private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken) + { + foreach (var (uriInWorkspace, documentsForUri) in documentsInWorkspace) + { + // We're comparing text, so we can take any of the linked documents. + var firstDocument = documentsForUri.First(); + var isTextEquivalent = await AreChecksumsEqualAsync(firstDocument, _trackedDocuments[uriInWorkspace].Text, cancellationToken).ConfigureAwait(false); + + if (!isTextEquivalent) + { + _logger.LogWarning($"Text for {uriInWorkspace} did not match document text {firstDocument.Id} in workspace's {firstDocument.Project.Solution.WorkspaceKind} current solution"); + return false; + } + } + + return true; + } + + private static async ValueTask AreChecksumsEqualAsync(TextDocument document, SourceText lspText, CancellationToken cancellationToken) + { + var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (documentText == lspText) + return true; + + return lspText.GetContentHash().AsSpan().SequenceEqual(documentText.GetContentHash().AsSpan()); + } + + #endregion + + /// + /// Returns a Roslyn language name for the given URI. + /// + internal string GetLanguageForUri(Uri uri) + { + string? languageId = null; + if (_trackedDocuments.TryGetValue(uri, out var trackedDocument)) + { + languageId = trackedDocument.LanguageId; + } + + var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri); + return _languageInfoProvider.GetLanguageInformation(documentFilePath, languageId).LanguageName; + } + + /// + /// Using the workspace's current solutions, find the matching documents in for each URI. + /// + private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution) + { + using var _ = PooledDictionary>.GetInstance(out var documentsInSolution); + foreach (var trackedDoc in trackedDocuments) + { + var documents = workspaceCurrentSolution.GetTextDocuments(trackedDoc); + if (documents.Any()) + { + documentsInSolution[trackedDoc] = documents; + } + } + + return documentsInSolution.ToImmutableDictionary(); + } + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor + { + private readonly LspWorkspaceManager _manager; + + public TestAccessor(LspWorkspaceManager manager) + => _manager = manager; + + public LspMiscellaneousFilesWorkspace? GetLspMiscellaneousFilesWorkspace() + => _manager._lspMiscellaneousFilesWorkspace; + + public bool IsWorkspaceRegistered(Workspace workspace) + { + return _manager._lspWorkspaceRegistrationService.GetAllRegistrations().Contains(workspace); + } + } + */ +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/LspWorkspaceManagerFactory.cs b/MSBuildLanguageServer/Import/LspWorkspaceManagerFactory.cs new file mode 100644 index 00000000..248e4e2e --- /dev/null +++ b/MSBuildLanguageServer/Import/LspWorkspaceManagerFactory.cs @@ -0,0 +1,42 @@ +// based on +// https://raw.githubusercontent.com/dotnet/roslyn/b9c35f1021d2d9d40521474c388d49ee7580ec98/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManagerFactory.cs +// with some parts commented out with /* */ comments + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(LspWorkspaceManager)), Shared] +internal class LspWorkspaceManagerFactory : ILspServiceFactory +{ + /* + private readonly LspWorkspaceRegistrationService _workspaceRegistrationService; + */ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LspWorkspaceManagerFactory(/*LspWorkspaceRegistrationService lspWorkspaceRegistrationService*/) + { + /* + _workspaceRegistrationService = lspWorkspaceRegistrationService; + */ + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + /* + var miscFilesWorkspace = lspServices.GetService(); + var languageInfoProvider = lspServices.GetRequiredService(); + */ + var telemetryLogger = lspServices.GetRequiredService(); + return new LspWorkspaceManager(logger, /*miscFilesWorkspace, _workspaceRegistrationService, languageInfoProvider,*/ telemetryLogger); + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/MSBuildLanguageServer.cs b/MSBuildLanguageServer/Import/MSBuildLanguageServer.cs new file mode 100644 index 00000000..6d58c1c8 --- /dev/null +++ b/MSBuildLanguageServer/Import/MSBuildLanguageServer.cs @@ -0,0 +1,246 @@ +// modified copy of +// https://github.com/dotnet/roslyn/blob/12f89683716864af2582b59f9b94395ad8f39910/src/LanguageServer/Protocol/RoslynLanguageServer.cs +// changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.ServerLifetime; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer +{ + internal sealed class MSBuildLanguageServer : SystemTextJsonLanguageServer, IOnInitialized + { + private readonly AbstractLspServiceProvider _lspServiceProvider; + private readonly FrozenDictionary> _baseServices; + private readonly WellKnownLspServerKinds _serverKind; + + public MSBuildLanguageServer( + AbstractLspServiceProvider lspServiceProvider, + JsonRpc jsonRpc, + JsonSerializerOptions serializerOptions, + ICapabilitiesProvider capabilitiesProvider, + AbstractLspLogger logger, + HostServices hostServices, + ImmutableArray supportedLanguages, + WellKnownLspServerKinds serverKind, + AbstractTypeRefResolver? typeRefResolver = null) + : base(jsonRpc, serializerOptions, logger, typeRefResolver) + { + _lspServiceProvider = lspServiceProvider; + _serverKind = serverKind; + + // Create services that require base dependencies (jsonrpc) or are more complex to create to the set manually. + _baseServices = GetBaseServices(jsonRpc, logger, capabilitiesProvider, hostServices, serverKind, supportedLanguages); + + // This spins up the queue and ensure the LSP is ready to start receiving requests + Initialize(); + } + + // MODIFICATION: added option to suppress VS extension converters + public static SystemTextJsonFormatter CreateJsonMessageFormatter(bool excludeVSExtensionConverters = false) + { + var messageFormatter = new SystemTextJsonFormatter(); + messageFormatter.JsonSerializerOptions.AddLspSerializerOptions(excludeVSExtensionConverters); + return messageFormatter; + } + + protected override ILspServices ConstructLspServices() + { + return _lspServiceProvider.CreateServices(_serverKind, _baseServices); + } + + protected override IRequestExecutionQueue ConstructRequestExecutionQueue() + { + var provider = GetLspServices().GetRequiredService>(); + return provider.CreateRequestExecutionQueue(this, Logger, HandlerProvider); + } + + private FrozenDictionary> GetBaseServices( + JsonRpc jsonRpc, + AbstractLspLogger logger, + ICapabilitiesProvider capabilitiesProvider, + HostServices hostServices, + WellKnownLspServerKinds serverKind, + ImmutableArray supportedLanguages) + { + // This map will hold either a single BaseService instance, or an ImmutableArray.Builder. + var baseServiceMap = new Dictionary(); + + var clientLanguageServerManager = new ClientLanguageServerManager(jsonRpc); + var lifeCycleManager = new LspServiceLifeCycleManager(clientLanguageServerManager); + + AddService(clientLanguageServerManager); + AddService(logger); + AddService(logger); + AddService(capabilitiesProvider); + AddService(lifeCycleManager); + AddService(new ServerInfoProvider(serverKind, supportedLanguages)); + AddLazyService>((lspServices) => new RequestContextFactory(lspServices)); + AddLazyService>((_) => GetRequestExecutionQueue()); + AddLazyService((lspServices) => new TelemetryService(lspServices)); + AddService(new InitializeManager()); + AddService(new InitializeHandler()); + AddService(new InitializedHandler()); + AddService(this); + + // MODIFICATION 1 + /* + AddService(new LanguageInfoProvider()); + + // In all VS cases, we already have a misc workspace. Specifically + // Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.MiscellaneousFilesWorkspace. In + // those cases, we do not need to add an additional workspace to manage new files we hear about. So only + // add the LspMiscellaneousFilesWorkspace for hosts that have not already brought their own. + if (serverKind == WellKnownLspServerKinds.CSharpVisualBasicLspServer) + AddLazyService(lspServices => new LspMiscellaneousFilesWorkspace(lspServices, hostServices)); + */ + + return baseServiceMap.ToFrozenDictionary( + keySelector: kvp => kvp.Key, + elementSelector: kvp => kvp.Value switch + { + BaseService service => [service], + ImmutableArray.Builder builder => builder.ToImmutable(), + _ => throw ExceptionUtilities.Unreachable() + }); + + void AddService(T instance) + where T : class + { + AddBaseService(BaseService.Create(instance)); + } + + void AddLazyService(Func creator) + where T : class + { + AddBaseService(BaseService.CreateLazily(creator)); + } + + void AddBaseService(BaseService baseService) + { + var typeName = baseService.Type.FullName; + Contract.ThrowIfNull(typeName); + + // If the service doesn't exist in the map yet, just add it. + if (!baseServiceMap.TryGetValue(typeName, out var value)) + { + baseServiceMap.Add(typeName, baseService); + return; + } + + // If the service exists in the map, check to see if it's a... + switch (value) + { + // ... BaseService. In this case, update the map with an ImmutableArray.Builder + // and add both the existing and new services to it. + case BaseService existingService: + var builder = ImmutableArray.CreateBuilder(); + builder.Add(existingService); + builder.Add(baseService); + + baseServiceMap[typeName] = builder; + break; + + // ... ImmutableArray.Builder. In this case, just add the new service to the builder. + case ImmutableArray.Builder existingBuilder: + existingBuilder.Add(baseService); + break; + + default: + throw ExceptionUtilities.Unreachable(); + } + } + } + + public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + OnInitialized(); + return Task.CompletedTask; + } + + // MODIFICATION 2 + /* + protected override string GetLanguageForRequest(string methodName, JsonElement? parameters) + { + if (parameters == null) + { + Logger.LogInformation("No request parameters given, using default language handler"); + return LanguageServerConstants.DefaultLanguageName; + } + + // For certain requests like text syncing we'll always use the default language handler + // as we do not want languages to be able to override them. + if (ShouldUseDefaultLanguage(methodName)) + { + return LanguageServerConstants.DefaultLanguageName; + } + + var lspWorkspaceManager = GetLspServices().GetRequiredService(); + + // All general LSP spec document params have the following json structure + // { "textDocument": { "uri": "" ... } ... } + // + // We can easily identify the URI for the request by looking for this structure + if (parameters.Value.TryGetProperty("textDocument", out var textDocumentToken) || + parameters.Value.TryGetProperty("_vs_textDocument", out textDocumentToken)) + { + var uriToken = textDocumentToken.GetProperty("uri"); + var uri = JsonSerializer.Deserialize(uriToken, ProtocolConversions.LspJsonSerializerOptions); + Contract.ThrowIfNull(uri, "Failed to deserialize uri property"); + var language = lspWorkspaceManager.GetLanguageForUri(uri); + Logger.LogInformation($"Using {language} from request text document"); + return language; + } + + // All the LSP resolve params have the following known json structure + // { "data": { "TextDocument": { "uri": "" ... } ... } ... } + // + // We can deserialize the data object using our unified DocumentResolveData. + //var dataToken = parameters["data"]; + if (parameters.Value.TryGetProperty("data", out var dataToken)) + { + var data = JsonSerializer.Deserialize(dataToken, ProtocolConversions.LspJsonSerializerOptions); + Contract.ThrowIfNull(data, "Failed to document resolve data object"); + var language = lspWorkspaceManager.GetLanguageForUri(data.TextDocument.Uri); + Logger.LogInformation($"Using {language} from data text document"); + return language; + } + + // This request is not for a textDocument and is not a resolve request. + Logger.LogInformation("Request did not contain a textDocument, using default language handler"); + return LanguageServerConstants.DefaultLanguageName; + + static bool ShouldUseDefaultLanguage(string methodName) + { + return methodName switch + { + Methods.InitializeName => true, + Methods.InitializedName => true, + Methods.TextDocumentDidOpenName => true, + Methods.TextDocumentDidChangeName => true, + Methods.TextDocumentDidCloseName => true, + Methods.TextDocumentDidSaveName => true, + Methods.ShutdownName => true, + Methods.ExitName => true, + _ => false, + }; + } + } + */ + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/MefLanguageServices.cs b/MSBuildLanguageServer/Import/MefLanguageServices.cs new file mode 100644 index 00000000..bf20b9fd --- /dev/null +++ b/MSBuildLanguageServer/Import/MefLanguageServices.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +/* +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +using Roslyn.Utilities; + +using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; + +[assembly: DebuggerTypeProxy(typeof(MefLanguageServices.LazyServiceMetadataDebuggerProxy), Target = typeof(ImmutableArray>))] + +namespace Microsoft.CodeAnalysis.Host.Mef.Modified +{ + internal sealed class MefLanguageServices : HostLanguageServices + { + private readonly MefWorkspaceServices _workspaceServices; + private readonly string _language; + private readonly ImmutableArray<(Lazy lazyService, bool usesFactory)> _services; + + private ImmutableDictionary? lazyService, bool usesFactory)> _serviceMap + = ImmutableDictionary? lazyService, bool usesFactory)>.Empty; + + private readonly object _gate = new(); + private readonly HashSet _ownedDisposableServices = new(ReferenceEqualityComparer.Instance); + + public MefLanguageServices( + MefWorkspaceServices workspaceServices, + string language) + { + _workspaceServices = workspaceServices; + _language = language; + + var hostServices = workspaceServices.HostExportProvider; + + var services = hostServices.GetExports() + .Select(lz => (lazyService: lz, usesFactory: false)); + var factories = hostServices.GetExports() + .Select(lz => (lazyService: new Lazy(() => lz.Value.CreateLanguageService(this), lz.Metadata), usesFactory: true)); + + _services = services.Concat(factories).Where(lz => lz.lazyService.Metadata.Language == language).ToImmutableArray(); + } + + public override HostWorkspaceServices WorkspaceServices => _workspaceServices; + + public override string Language => _language; + + public bool HasServices + { + get { return _services.Length > 0; } + } + + + public override void Dispose() + { + ImmutableArray disposableServices; + lock(_gate) + { + disposableServices = _ownedDisposableServices.ToImmutableArray(); + _ownedDisposableServices.Clear(); + } + + // Take care to give all disposal parts a chance to dispose even if some parts throw exceptions. + List? exceptions = null; + foreach(var service in disposableServices) + { + MefUtilities.DisposeWithExceptionTracking(service, ref exceptions); + } + + if(exceptions is not null) + { + throw new AggregateException(CompilerExtensionsResources.Instantiated_parts_threw_exceptions_from_IDisposable_Dispose, exceptions); + } + + base.Dispose(); + } + + public override TLanguageService GetService() + { + if(TryGetService(static _ => true, out var service)) + { + return service; + } else + { + return default!; + } + } + + internal bool TryGetService(HostWorkspaceServices.MetadataFilter filter, [MaybeNullWhen(false)] out TLanguageService languageService) + { + if(TryGetService(typeof(TLanguageService), out var lazyService, out var usesFactory) + && filter(lazyService.Metadata.Data)) + { + // MEF language service instances created by a factory are not owned by the MEF catalog or disposed + // when the MEF catalog is disposed. Whenever we are potentially going to create an instance of a + // service provided by a factory, we need to check if the resulting service implements IDisposable. The + // specific conditions here are: + // + // * usesFactory: This is true when the language service is provided by a factory. Services provided + // directly are owned by the MEF catalog so they do not need to be tracked by the workspace. + // * IsValueCreated: This will be false at least once prior to accessing the lazy value. Once the value + // is known to be created, we no longer need to try adding it to _ownedDisposableServices, so we use a + // lock-free fast path. + var checkAddDisposable = usesFactory && !lazyService.IsValueCreated; + + languageService = (TLanguageService)lazyService.Value; + if(checkAddDisposable && languageService is IDisposable disposable) + { + lock(_gate) + { + _ownedDisposableServices.Add(disposable); + } + } + + return true; + } else + { + languageService = default; + return false; + } + } + + private bool TryGetService(Type serviceType, [NotNullWhen(true)] out Lazy? lazyService, out bool usesFactory) + { + if(!_serviceMap.TryGetValue(serviceType, out var service)) + { + service = ImmutableInterlocked.GetOrAdd(ref _serviceMap, serviceType, serviceType => LayeredServiceUtilities.PickService(serviceType, _workspaceServices.WorkspaceKind, _services)); + } + + (lazyService, usesFactory) = (service.lazyService, service.usesFactory); + return lazyService != null; + } + + internal sealed class LazyServiceMetadataDebuggerProxy(ImmutableArray> services) + { + public (string type, string layer)[] Metadata + => services.Select(s => (s.Metadata.ServiceType, s.Metadata.Layer)).ToArray(); + } + } +}*/ \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/Program.cs b/MSBuildLanguageServer/Import/Program.cs new file mode 100644 index 00000000..90c1c78e --- /dev/null +++ b/MSBuildLanguageServer/Import/Program.cs @@ -0,0 +1,318 @@ +// modified copy of +// https://raw.githubusercontent.com/dotnet/roslyn/12f89683716864af2582b59f9b94395ad8f39910/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs +// changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.CommandLine; +using System.Diagnostics; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Text.Json; + +// BEGIN MODIFICATION 1 +/* +using Microsoft.CodeAnalysis.Contracts.Telemetry; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +*/ +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Roslyn.Utilities; + +// Setting the title can fail if the process is run without a window, such +// as when launched detached from nodejs +try +{ + // BEGIN MODIFICATION 2 + Console.Title = "MSBuildLanguageServer"; + // END MODIFICATION 2 +} +catch (IOException) +{ +} + +WindowsErrorReporting.SetErrorModeOnWindows(); + +var parser = CreateCommandLineParser(); +return await parser.Parse(args).InvokeAsync(CancellationToken.None); + +static async Task RunAsync(ServerConfiguration serverConfiguration, CancellationToken cancellationToken) +{ + // Before we initialize the LSP server we can't send LSP log messages. + // Create a console logger as a fallback to use before the LSP server starts. + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(serverConfiguration.MinimumLogLevel); + builder.AddProvider(new LspLogMessageLoggerProvider(fallbackLoggerFactory: + // Add a console logger as a fallback for when the LSP server has not finished initializing. + LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(serverConfiguration.MinimumLogLevel); + builder.AddConsole(); + // The console logger outputs control characters on unix for colors which don't render correctly in VSCode. + builder.AddSimpleConsole(formatterOptions => formatterOptions.ColorBehavior = LoggerColorBehavior.Disabled); + }) + )); + }); + + var logger = loggerFactory.CreateLogger(); + + logger.Log(serverConfiguration.LaunchDebugger ? LogLevel.Critical : LogLevel.Trace, "Server started with process ID {processId}", Environment.ProcessId); + if (serverConfiguration.LaunchDebugger) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Debugger.Launch() only works on Windows. + _ = Debugger.Launch(); + } + else + { + var timeout = TimeSpan.FromMinutes(2); + logger.LogCritical($"Waiting {timeout:g} for a debugger to attach"); + using var timeoutSource = new CancellationTokenSource(timeout); + while (!Debugger.IsAttached && !timeoutSource.Token.IsCancellationRequested) + { + await Task.Delay(100, CancellationToken.None); + } + } + } + + logger.LogTrace($".NET Runtime Version: {RuntimeInformation.FrameworkDescription}"); + var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory); + var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); + var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, loggerFactory); + + using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, loggerFactory); + + /* + // LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to + // 'automatic' for now. + var globalOptionService = exportProvider.GetExportedValue(); + globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Automatic); + */ + + // The log file directory passed to us by VSCode might not exist yet, though its parent directory is guaranteed to exist. + Directory.CreateDirectory(serverConfiguration.ExtensionLogDirectory); + // BEGIN MODIFICATION + /* + // Initialize the server configuration MEF exported value. + exportProvider.GetExportedValue().InitializeConfiguration(serverConfiguration); + + // Initialize the fault handler if it's available + var telemetryReporter = exportProvider.GetExports().SingleOrDefault()?.Value; + RoslynLogger.Initialize(telemetryReporter, serverConfiguration.TelemetryLevel, serverConfiguration.SessionId); + + // Create the workspace first, since right now the language server will assume there's at least one Workspace + var workspaceFactory = exportProvider.GetExportedValue(); + + var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll") + .Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer", StringComparison.Ordinal)) + .Select(f => f.FullName) + .ToImmutableArray(); + + // Include analyzers from extension assemblies. + analyzerPaths = analyzerPaths.AddRange(extensionManager.ExtensionAssemblyPaths); + + await workspaceFactory.InitializeSolutionLevelAnalyzersAsync(analyzerPaths, extensionManager); + + var serviceBrokerFactory = exportProvider.GetExportedValue(); + StarredCompletionAssemblyHelper.InitializeInstance(serverConfiguration.StarredCompletionsPath, extensionManager, loggerFactory, serviceBrokerFactory); + // TODO: Remove, the path should match exactly. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914. + Microsoft.CodeAnalysis.EditAndContinue.EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames = Path.DirectorySeparatorChar == '\\'; + */ + + var languageServerLogger = loggerFactory.CreateLogger(nameof(LanguageServerHost)); + + var (clientPipeName, serverPipeName) = CreateNewPipeNames(); + var pipeServer = new NamedPipeServerStream(serverPipeName, + PipeDirection.InOut, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); + + // Send the named pipe connection info to the client + Console.WriteLine(JsonSerializer.Serialize(new NamedPipeInformation(clientPipeName))); + + // Wait for connection from client + await pipeServer.WaitForConnectionAsync(cancellationToken); + + var server = new LanguageServerHost(pipeServer, pipeServer, exportProvider, languageServerLogger, typeRefResolver); + server.Start(); + + logger.LogInformation("Language server initialized"); + + try + { + await server.WaitForExitAsync(); + } + finally + { + // MODIFICATION + /* + // After the LSP server shutdown, report session wide telemetry + RoslynLogger.ShutdownAndReportSessionTelemetry(); + + // Server has exited, cancel our service broker service + await serviceBrokerFactory.ShutdownAndWaitForCompletionAsync(); + */ + } +} + +static CliRootCommand CreateCommandLineParser() +{ + var debugOption = new CliOption("--debug") + { + Description = "Flag indicating if the debugger should be launched on startup.", + Required = false, + DefaultValueFactory = _ => false, + }; + var brokeredServicePipeNameOption = new CliOption("--brokeredServicePipeName") + { + Description = "The name of the pipe used to connect to a remote process (if one exists).", + Required = false, + }; + + var logLevelOption = new CliOption("--logLevel") + { + Description = "The minimum log verbosity.", + Required = true, + }; + var starredCompletionsPathOption = new CliOption("--starredCompletionComponentPath") + { + Description = "The location of the starred completion component (if one exists).", + Required = false, + }; + + var telemetryLevelOption = new CliOption("--telemetryLevel") + { + Description = "Telemetry level, Defaults to 'off'. Example values: 'all', 'crash', 'error', or 'off'.", + Required = false, + }; + var extensionLogDirectoryOption = new CliOption("--extensionLogDirectory") + { + Description = "The directory where we should write log files to", + Required = true, + }; + + var sessionIdOption = new CliOption("--sessionId") + { + Description = "Session Id to use for telemetry", + Required = false + }; + + var extensionAssemblyPathsOption = new CliOption("--extension") + { + Description = "Full paths of extension assemblies to load (optional).", + Required = false + }; + + var devKitDependencyPathOption = new CliOption("--devKitDependencyPath") + { + Description = "Full path to the Roslyn dependency used with DevKit (optional).", + Required = false + }; + + var razorSourceGeneratorOption = new CliOption("--razorSourceGenerator") + { + Description = "Full path to the Razor source generator (optional).", + Required = false + }; + + var razorDesignTimePathOption = new CliOption("--razorDesignTimePath") + { + Description = "Full path to the Razor design time target path (optional).", + Required = false + }; + + var rootCommand = new CliRootCommand() + { + debugOption, + brokeredServicePipeNameOption, + logLevelOption, + starredCompletionsPathOption, + telemetryLevelOption, + sessionIdOption, + extensionAssemblyPathsOption, + devKitDependencyPathOption, + razorSourceGeneratorOption, + razorDesignTimePathOption, + extensionLogDirectoryOption + }; + rootCommand.SetAction((parseResult, cancellationToken) => + { + var launchDebugger = parseResult.GetValue(debugOption); + var logLevel = parseResult.GetValue(logLevelOption); + var starredCompletionsPath = parseResult.GetValue(starredCompletionsPathOption); + var telemetryLevel = parseResult.GetValue(telemetryLevelOption); + var sessionId = parseResult.GetValue(sessionIdOption); + var extensionAssemblyPaths = parseResult.GetValue(extensionAssemblyPathsOption) ?? []; + var devKitDependencyPath = parseResult.GetValue(devKitDependencyPathOption); + var razorSourceGenerator = parseResult.GetValue(razorSourceGeneratorOption); + var razorDesignTimePath = parseResult.GetValue(razorDesignTimePathOption); + var extensionLogDirectory = parseResult.GetValue(extensionLogDirectoryOption)!; + + var serverConfiguration = new ServerConfiguration( + LaunchDebugger: launchDebugger, + MinimumLogLevel: logLevel, + StarredCompletionsPath: starredCompletionsPath, + TelemetryLevel: telemetryLevel, + SessionId: sessionId, + ExtensionAssemblyPaths: extensionAssemblyPaths, + DevKitDependencyPath: devKitDependencyPath, + RazorSourceGenerator: razorSourceGenerator, + RazorDesignTimePath: razorDesignTimePath, + ExtensionLogDirectory: extensionLogDirectory); + + return RunAsync(serverConfiguration, cancellationToken); + }); + return rootCommand; +} + +static (string clientPipe, string serverPipe) CreateNewPipeNames() +{ + // On windows, .NET and Nodejs use different formats for the pipe name + const string WINDOWS_NODJS_PREFIX = @"\\.\pipe\"; + const string WINDOWS_DOTNET_PREFIX = @"\\.\"; + + // The pipe name constructed by some systems is very long (due to temp path). + // Shorten the unique id for the pipe. + var newGuid = Guid.NewGuid().ToString(); + var pipeName = newGuid.Split('-')[0]; + + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (WINDOWS_NODJS_PREFIX + pipeName, WINDOWS_DOTNET_PREFIX + pipeName) + : (GetUnixTypePipeName(pipeName), GetUnixTypePipeName(pipeName)); +} + +static string GetUnixTypePipeName(string pipeName) +{ + // Unix-type pipes are actually writing to a file + return Path.Combine(Path.GetTempPath(), pipeName + ".sock"); +} + +// MODIFICATION +// this was defined in a file we have not imported +internal record class ServerConfiguration( + bool LaunchDebugger, + LogLevel MinimumLogLevel, + string? StarredCompletionsPath, + string? TelemetryLevel, + string? SessionId, + IEnumerable ExtensionAssemblyPaths, + string? DevKitDependencyPath, + string? RazorSourceGenerator, + string? ExtensionLogDirectory, + string? RazorDesignTimePath = null); +// END MODIFICATION \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/ProtocolConversions.cs b/MSBuildLanguageServer/Import/ProtocolConversions.cs new file mode 100644 index 00000000..1e2b414f --- /dev/null +++ b/MSBuildLanguageServer/Import/ProtocolConversions.cs @@ -0,0 +1,1042 @@ +// modified copy of +// https://raw.githubusercontent.com/dotnet/roslyn/12f89683716864af2582b59f9b94395ad8f39910/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +// changes annotated inline with // MODIFICATION +// with portions commented out using /* */ comments + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +/* +using Microsoft.CodeAnalysis.DocumentHighlighting; +*/ +using Microsoft.CodeAnalysis.ErrorReporting; +/* +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.NavigateTo; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Tags; +*/ +using Microsoft.CodeAnalysis.Text; +/* +using Roslyn.Text.Adornments; +*/ +using Roslyn.Utilities; +using Logger = Microsoft.CodeAnalysis.Internal.Log.Logger; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer +{ + internal static partial class ProtocolConversions + { + private const string CSharpMarkdownLanguageName = "csharp"; + private const string VisualBasicMarkdownLanguageName = "vb"; + private const string SourceGeneratedDocumentBaseUri = "source-generated:///"; + private const string BlockCodeFence = "```"; + private const string InlineCodeFence = "`"; + +#pragma warning disable RS0030 // Do not use banned APIs + private static readonly Uri s_sourceGeneratedDocumentBaseUri = new(SourceGeneratedDocumentBaseUri, UriKind.Absolute); +#pragma warning restore + + private static readonly char[] s_dirSeparators = [PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar]; + + private static readonly Regex s_markdownEscapeRegex = new(@"([\\`\*_\{\}\[\]\(\)#+\-\.!])", RegexOptions.Compiled); + + // MODIFICATION: added accessor for markdown escape regex + public static string EscapeMarkdown(string unescaped) => s_markdownEscapeRegex.Replace(unescaped, @"\$1"); +/* + // NOTE: While the spec allows it, don't use Function and Method, as both VS and VS Code display them the same + // way which can confuse users + + /// + /// Mapping from tags to lsp completion item kinds. The value lists the potential lsp kinds from + /// least-preferred to most preferred. More preferred kinds will be chosen if the client states they support + /// it. This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP + /// spec). + /// + public static readonly ImmutableDictionary> RoslynTagToCompletionItemKinds = new Dictionary>() + { + { WellKnownTags.Public, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) }, + { WellKnownTags.Protected, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) }, + { WellKnownTags.Private, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) }, + { WellKnownTags.Internal, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) }, + { WellKnownTags.File, ImmutableArray.Create(LSP.CompletionItemKind.File) }, + { WellKnownTags.Project, ImmutableArray.Create(LSP.CompletionItemKind.File) }, + { WellKnownTags.Folder, ImmutableArray.Create(LSP.CompletionItemKind.Folder) }, + { WellKnownTags.Assembly, ImmutableArray.Create(LSP.CompletionItemKind.File) }, + { WellKnownTags.Class, ImmutableArray.Create(LSP.CompletionItemKind.Class) }, + { WellKnownTags.Constant, ImmutableArray.Create(LSP.CompletionItemKind.Constant) }, + { WellKnownTags.Delegate, ImmutableArray.Create(LSP.CompletionItemKind.Class, LSP.CompletionItemKind.Delegate) }, + { WellKnownTags.Enum, ImmutableArray.Create(LSP.CompletionItemKind.Enum) }, + { WellKnownTags.EnumMember, ImmutableArray.Create(LSP.CompletionItemKind.EnumMember) }, + { WellKnownTags.Event, ImmutableArray.Create(LSP.CompletionItemKind.Event) }, + { WellKnownTags.ExtensionMethod, ImmutableArray.Create(LSP.CompletionItemKind.Method, LSP.CompletionItemKind.ExtensionMethod) }, + { WellKnownTags.Field, ImmutableArray.Create(LSP.CompletionItemKind.Field) }, + { WellKnownTags.Interface, ImmutableArray.Create(LSP.CompletionItemKind.Interface) }, + { WellKnownTags.Intrinsic, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.Keyword, ImmutableArray.Create(LSP.CompletionItemKind.Keyword) }, + { WellKnownTags.Label, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.Local, ImmutableArray.Create(LSP.CompletionItemKind.Variable) }, + { WellKnownTags.Namespace, ImmutableArray.Create(LSP.CompletionItemKind.Module, LSP.CompletionItemKind.Namespace) }, + { WellKnownTags.Method, ImmutableArray.Create(LSP.CompletionItemKind.Method) }, + { WellKnownTags.Module, ImmutableArray.Create(LSP.CompletionItemKind.Module) }, + { WellKnownTags.Operator, ImmutableArray.Create(LSP.CompletionItemKind.Operator) }, + { WellKnownTags.Parameter, ImmutableArray.Create(LSP.CompletionItemKind.Value) }, + { WellKnownTags.Property, ImmutableArray.Create(LSP.CompletionItemKind.Property) }, + { WellKnownTags.RangeVariable, ImmutableArray.Create(LSP.CompletionItemKind.Variable) }, + { WellKnownTags.Reference, ImmutableArray.Create(LSP.CompletionItemKind.Reference) }, + { WellKnownTags.Structure, ImmutableArray.Create(LSP.CompletionItemKind.Struct) }, + { WellKnownTags.TypeParameter, ImmutableArray.Create(LSP.CompletionItemKind.TypeParameter) }, + { WellKnownTags.Snippet, ImmutableArray.Create(LSP.CompletionItemKind.Snippet) }, + { WellKnownTags.Error, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.Warning, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.StatusInformation, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.AddReference, ImmutableArray.Create(LSP.CompletionItemKind.Text) }, + { WellKnownTags.NuGet, ImmutableArray.Create(LSP.CompletionItemKind.Text) } + }.ToImmutableDictionary(); + + /// + /// Mapping from tags to LSP completion item tags. The value lists the potential LSP tags from + /// least-preferred to most preferred. More preferred kinds will be chosen if the client states they support + /// it. This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP + /// spec). + /// + public static readonly ImmutableDictionary> RoslynTagToCompletionItemTags = new Dictionary>() + { + { WellKnownTags.Deprecated, ImmutableArray.Create(LSP.CompletionItemTag.Deprecated) }, + }.ToImmutableDictionary(); + */ + + // MODIFICATION: added option to suppress VS extensions + public static JsonSerializerOptions AddLspSerializerOptions(this JsonSerializerOptions options, bool excludeVSExtensionConverters = false) + { + if (!excludeVSExtensionConverters) + { + LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(options); + } + options.Converters.Add(new LSP.NaturalObjectConverter()); + return options; + } + + /// + /// Options that know how to serialize / deserialize basic LSP types. + /// Useful when there are particular fields that are not serialized or deserialized by normal request handling (for example + /// deserializing a field that is typed as object instead of a concrete type). + /// + public static JsonSerializerOptions LspJsonSerializerOptions = new JsonSerializerOptions().AddLspSerializerOptions(); + + /* + // TO-DO: More LSP.CompletionTriggerKind mappings are required to properly map to Roslyn CompletionTriggerKinds. + // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1178726 + public static async Task LSPToRoslynCompletionTriggerAsync( + LSP.CompletionContext? context, + Document document, + int position, + CancellationToken cancellationToken) + { + if (context is null) + { + // Some LSP clients don't support sending extra context, so all we can do is invoke + return Completion.CompletionTrigger.Invoke; + } + else if (context.TriggerKind is LSP.CompletionTriggerKind.Invoked or LSP.CompletionTriggerKind.TriggerForIncompleteCompletions) + { + if (context is not LSP.VSInternalCompletionContext vsCompletionContext) + { + return Completion.CompletionTrigger.Invoke; + } + + switch (vsCompletionContext.InvokeKind) + { + case LSP.VSInternalCompletionInvokeKind.Explicit: + return Completion.CompletionTrigger.Invoke; + + case LSP.VSInternalCompletionInvokeKind.Typing: + var insertionChar = await GetInsertionCharacterAsync(document, position, cancellationToken).ConfigureAwait(false); + return Completion.CompletionTrigger.CreateInsertionTrigger(insertionChar); + + case LSP.VSInternalCompletionInvokeKind.Deletion: + Contract.ThrowIfNull(context.TriggerCharacter); + Contract.ThrowIfFalse(char.TryParse(context.TriggerCharacter, out var triggerChar)); + return Completion.CompletionTrigger.CreateDeletionTrigger(triggerChar); + + default: + // LSP added an InvokeKind that we need to support. + Logger.Log(FunctionId.LSPCompletion_MissingLSPCompletionInvokeKind); + return Completion.CompletionTrigger.Invoke; + } + } + else if (context.TriggerKind is LSP.CompletionTriggerKind.TriggerCharacter) + { + Contract.ThrowIfNull(context.TriggerCharacter); + Contract.ThrowIfFalse(char.TryParse(context.TriggerCharacter, out var triggerChar)); + return Completion.CompletionTrigger.CreateInsertionTrigger(triggerChar); + } + else + { + // LSP added a TriggerKind that we need to support. + Logger.Log(FunctionId.LSPCompletion_MissingLSPCompletionTriggerKind); + return Completion.CompletionTrigger.Invoke; + } + + // Local functions + static async Task GetInsertionCharacterAsync(Document document, int position, CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + // We use 'position - 1' here since we want to find the character that was just inserted. + Contract.ThrowIfTrue(position < 1); + var triggerCharacter = text[position - 1]; + return triggerCharacter; + } + } + */ + + public static string GetDocumentFilePathFromUri(Uri uri) + => uri.IsFile ? uri.LocalPath : uri.AbsoluteUri; + + /// + /// Converts an absolute local file path or an absolute URL string to . + /// + /// + /// The can't be represented as . + /// For example, UNC paths with invalid characters in server name. + /// + public static Uri CreateAbsoluteUri(string absolutePath) + { + var uriString = IsAscii(absolutePath) ? absolutePath : GetAbsoluteUriString(absolutePath); + try + { +#pragma warning disable RS0030 // Do not use banned APIs + return new(uriString, UriKind.Absolute); +#pragma warning restore + + } + catch (UriFormatException e) + { + // The standard URI format exception does not include the failing path, however + // in pretty much all cases we need to know the URI string (and original string) in order to fix the issue. + throw new UriFormatException($"Failed create URI from '{uriString}'; original string: '{absolutePath}'", e); + } + } + + internal static Uri CreateRelativePatternBaseUri(string path) + { + // According to VSCode LSP RelativePattern spec, + // found at https://github.com/microsoft/vscode/blob/9e1974682eb84eebb073d4ae775bad1738c281f6/src/vscode-dts/vscode.d.ts#L2226 + // the baseUri should not end in a trailing separator, nor should it + // have any relative segmeents (., ..) + if (path[^1] == System.IO.Path.DirectorySeparatorChar) + { + path = path[..^1]; + } + + Debug.Assert(!path.Split(System.IO.Path.DirectorySeparatorChar).Any(p => p == "." || p == "..")); + + return CreateAbsoluteUri(path); + } + + // Implements workaround for https://github.com/dotnet/runtime/issues/89538: + internal static string GetAbsoluteUriString(string absolutePath) + { + if (!PathUtilities.IsAbsolute(absolutePath)) + { + return absolutePath; + } + + var parts = absolutePath.Split(s_dirSeparators); + + if (PathUtilities.IsUnixLikePlatform) + { + // Unix path: first part is empty, all parts should be escaped + return "file://" + string.Join("/", parts.Select(EscapeUriPart)); + } + + if (parts is ["", "", var serverName, ..]) + { + // UNC path: first non-empty part is server name and shouldn't be escaped + return "file://" + serverName + "/" + string.Join("/", parts.Skip(3).Select(EscapeUriPart)); + } + + // Drive-rooted path: first part is "C:" and shouldn't be escaped + return "file:///" + parts[0] + "/" + string.Join("/", parts.Skip(1).Select(EscapeUriPart)); + +#pragma warning disable SYSLIB0013 // Type or member is obsolete + static string EscapeUriPart(string stringToEscape) + => Uri.EscapeUriString(stringToEscape).Replace("#", "%23"); +#pragma warning restore + } + + public static Uri CreateUriFromSourceGeneratedFilePath(string filePath) + { + Debug.Assert(!PathUtilities.IsAbsolute(filePath)); + + // Fast path for common cases: + if (IsAscii(filePath)) + { +#pragma warning disable RS0030 // Do not use banned APIs + return new Uri(s_sourceGeneratedDocumentBaseUri, filePath); +#pragma warning restore + } + + // Workaround for https://github.com/dotnet/runtime/issues/89538: + + var parts = filePath.Split(s_dirSeparators); + var url = SourceGeneratedDocumentBaseUri + string.Join("/", parts.Select(Uri.EscapeDataString)); + +#pragma warning disable RS0030 // Do not use banned APIs + return new Uri(url, UriKind.Absolute); +#pragma warning restore + } + + private static bool IsAscii(char c) + => (uint)c <= '\x007f'; + + private static bool IsAscii(string filePath) + { + for (var i = 0; i < filePath.Length; i++) + { + if (!IsAscii(filePath[i])) + { + return false; + } + } + + return true; + } + + /* + public static LSP.TextDocumentPositionParams PositionToTextDocumentPositionParams(int position, SourceText text, Document document) + { + return new LSP.TextDocumentPositionParams() + { + TextDocument = DocumentToTextDocumentIdentifier(document), + Position = LinePositionToPosition(text.Lines.GetLinePosition(position)) + }; + } + + public static LSP.TextDocumentIdentifier DocumentToTextDocumentIdentifier(TextDocument document) + => new LSP.TextDocumentIdentifier { Uri = document.GetURI() }; + + public static LSP.VersionedTextDocumentIdentifier DocumentToVersionedTextDocumentIdentifier(Document document) + => new LSP.VersionedTextDocumentIdentifier { Uri = document.GetURI() }; + */ + + public static LinePosition PositionToLinePosition(LSP.Position position) + => new LinePosition(position.Line, position.Character); + public static LinePositionSpan RangeToLinePositionSpan(LSP.Range range) + => new(PositionToLinePosition(range.Start), PositionToLinePosition(range.End)); + + public static TextSpan RangeToTextSpan(LSP.Range range, SourceText text) + { + var linePositionSpan = RangeToLinePositionSpan(range); + + try + { + try + { + return text.Lines.GetTextSpan(linePositionSpan); + } + catch (ArgumentException ex) + { + // Create a custom error for this so we can examine the data we're getting. + throw new ArgumentException($"Range={RangeToString(range)}. text.Length={text.Length}. text.Lines.Count={text.Lines.Count}", ex); + } + } + // Temporary exception reporting to investigate https://github.com/dotnet/roslyn/issues/66258. + catch (Exception e) when (FatalError.ReportAndPropagate(e)) + { + throw; + } + + static string RangeToString(LSP.Range range) + => $"{{ Start={PositionToString(range.Start)}, End={PositionToString(range.End)} }}"; + + static string PositionToString(LSP.Position position) + => $"{{ Line={position.Line}, Character={position.Character} }}"; + } + + public static LSP.TextEdit TextChangeToTextEdit(TextChange textChange, SourceText oldText) + { + Contract.ThrowIfNull(textChange.NewText); + return new LSP.TextEdit + { + NewText = textChange.NewText, + Range = TextSpanToRange(textChange.Span, oldText) + }; + } + + public static TextChange TextEditToTextChange(LSP.TextEdit edit, SourceText oldText) + => new TextChange(RangeToTextSpan(edit.Range, oldText), edit.NewText); + + public static TextChange ContentChangeEventToTextChange(LSP.TextDocumentContentChangeEvent changeEvent, SourceText text) + => new TextChange(RangeToTextSpan(changeEvent.Range, text), changeEvent.Text); + + public static LSP.Position LinePositionToPosition(LinePosition linePosition) + => new LSP.Position { Line = linePosition.Line, Character = linePosition.Character }; + + public static LSP.Range LinePositionToRange(LinePositionSpan linePositionSpan) + => new LSP.Range { Start = LinePositionToPosition(linePositionSpan.Start), End = LinePositionToPosition(linePositionSpan.End) }; + + public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) + { + var linePosSpan = text.Lines.GetLinePositionSpan(textSpan); + return LinePositionToRange(linePosSpan); + } + /* + public static Task DocumentSpanToLocationAsync(DocumentSpan documentSpan, CancellationToken cancellationToken) + => TextSpanToLocationAsync(documentSpan.Document, documentSpan.SourceSpan, isStale: false, cancellationToken); + + public static async Task DocumentSpanToLocationWithTextAsync( + DocumentSpan documentSpan, ClassifiedTextElement text, CancellationToken cancellationToken) + { + var location = await TextSpanToLocationAsync( + documentSpan.Document, documentSpan.SourceSpan, isStale: false, cancellationToken).ConfigureAwait(false); + + return location == null ? null : new LSP.VSInternalLocation + { + Uri = location.Uri, + Range = location.Range, + Text = text + }; + } + + /// + /// Compute all the for the input list of changed documents. + /// Additionally maps the locations of the changed documents if necessary. + /// + public static async Task ChangedDocumentsToTextDocumentEditsAsync(IEnumerable changedDocuments, Func getNewDocumentFunc, + Func getOldDocumentFunc, IDocumentTextDifferencingService? textDiffService, CancellationToken cancellationToken) where T : TextDocument + { + using var _ = ArrayBuilder<(Uri Uri, LSP.TextEdit TextEdit)>.GetInstance(out var uriToTextEdits); + + foreach (var docId in changedDocuments) + { + var newDocument = getNewDocumentFunc(docId); + var oldDocument = getOldDocumentFunc(docId); + + var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + ImmutableArray textChanges; + + // Normal documents have a unique service for calculating minimal text edits. If we used the standard 'GetTextChanges' + // method instead, we would get a change that spans the entire document, which we ideally want to avoid. + if (newDocument is Document newDoc && oldDocument is Document oldDoc) + { + Contract.ThrowIfNull(textDiffService); + textChanges = await textDiffService.GetTextChangesAsync(oldDoc, newDoc, cancellationToken).ConfigureAwait(false); + } + else + { + var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + textChanges = [.. newText.GetTextChanges(oldText)]; + } + + // Map all the text changes' spans for this document. + var mappedResults = await GetMappedSpanResultAsync(oldDocument, textChanges.Select(tc => tc.Span).ToImmutableArray(), cancellationToken).ConfigureAwait(false); + if (mappedResults == null) + { + // There's no span mapping available, just create text edits from the original text changes. + foreach (var textChange in textChanges) + { + uriToTextEdits.Add((oldDocument.GetURI(), TextChangeToTextEdit(textChange, oldText))); + } + } + else + { + // We have mapping results, so create text edits from the mapped text change spans. + for (var i = 0; i < textChanges.Length; i++) + { + var mappedSpan = mappedResults.Value[i]; + var textChange = textChanges[i]; + if (!mappedSpan.IsDefault) + { + uriToTextEdits.Add((CreateAbsoluteUri(mappedSpan.FilePath), new LSP.TextEdit + { + Range = MappedSpanResultToRange(mappedSpan), + NewText = textChange.NewText ?? string.Empty + })); + } + } + } + } + + var documentEdits = uriToTextEdits.GroupBy(uriAndEdit => uriAndEdit.Uri, uriAndEdit => uriAndEdit.TextEdit, (uri, edits) => new LSP.TextDocumentEdit + { + TextDocument = new LSP.OptionalVersionedTextDocumentIdentifier { Uri = uri }, + Edits = edits.ToArray(), + }).ToArray(); + + return documentEdits; + } + + public static Task TextSpanToLocationAsync( + TextDocument document, + TextSpan textSpan, + bool isStale, + CancellationToken cancellationToken) + { + return TextSpanToLocationAsync(document, textSpan, isStale, context: null, cancellationToken); + } + + public static async Task TextSpanToLocationAsync( + TextDocument document, + TextSpan textSpan, + bool isStale, + RequestContext? context, + CancellationToken cancellationToken) + { + Debug.Assert(document.FilePath != null); + + var result = await GetMappedSpanResultAsync(document, [textSpan], cancellationToken).ConfigureAwait(false); + if (result == null) + return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); + + var mappedSpan = result.Value.Single(); + if (mappedSpan.IsDefault) + return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); + + Uri? uri = null; + try + { + if (PathUtilities.IsAbsolute(mappedSpan.FilePath)) + uri = CreateAbsoluteUri(mappedSpan.FilePath); + } + catch (UriFormatException) + { + } + + if (uri == null) + { + context?.TraceInformation($"Could not convert '{mappedSpan.FilePath}' to uri"); + return null; + } + + return new LSP.Location + { + Uri = uri, + Range = MappedSpanResultToRange(mappedSpan) + }; + + static async Task ConvertTextSpanToLocationAsync( + TextDocument document, + TextSpan span, + bool isStale, + CancellationToken cancellationToken) + { + Debug.Assert(document.FilePath != null); + var uri = document.GetURI(); + + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + if (isStale) + { + // in the case of a stale item, the span may be out of bounds of the document. Cap + // us to the end of the document as that's where we're going to navigate the user + // to. + span = TextSpan.FromBounds( + Math.Min(text.Length, span.Start), + Math.Min(text.Length, span.End)); + } + + return ConvertTextSpanWithTextToLocation(span, text, uri); + } + + static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText text, Uri documentUri) + { + var location = new LSP.Location + { + Uri = documentUri, + Range = TextSpanToRange(span, text), + }; + + return location; + } + } + */ + + public static LSP.CodeDescription? HelpLinkToCodeDescription(Uri? uri) + => (uri != null) ? new LSP.CodeDescription { Href = uri } : null; + /* + + public static LSP.SymbolKind NavigateToKindToSymbolKind(string kind) + { + if (Enum.TryParse(kind, out var symbolKind)) + { + return symbolKind; + } + + // TODO - Define conversion from NavigateToItemKind to LSP Symbol kind + switch (kind) + { + case NavigateToItemKind.EnumItem: + return LSP.SymbolKind.EnumMember; + case NavigateToItemKind.Structure: + return LSP.SymbolKind.Struct; + case NavigateToItemKind.Delegate: + return LSP.SymbolKind.Function; + default: + return LSP.SymbolKind.Object; + } + } + + public static LSP.DocumentHighlightKind HighlightSpanKindToDocumentHighlightKind(HighlightSpanKind kind) + { + switch (kind) + { + case HighlightSpanKind.Reference: + return LSP.DocumentHighlightKind.Read; + case HighlightSpanKind.WrittenReference: + return LSP.DocumentHighlightKind.Write; + default: + return LSP.DocumentHighlightKind.Text; + } + } + + public static Glyph SymbolKindToGlyph(LSP.SymbolKind kind) + { + switch (kind) + { + case LSP.SymbolKind.File: + return Glyph.CSharpFile; + case LSP.SymbolKind.Module: + return Glyph.ModulePublic; + case LSP.SymbolKind.Namespace: + return Glyph.Namespace; + case LSP.SymbolKind.Package: + return Glyph.Assembly; + case LSP.SymbolKind.Class: + return Glyph.ClassPublic; + case LSP.SymbolKind.Method: + return Glyph.MethodPublic; + case LSP.SymbolKind.Property: + return Glyph.PropertyPublic; + case LSP.SymbolKind.Field: + return Glyph.FieldPublic; + case LSP.SymbolKind.Constructor: + return Glyph.MethodPublic; + case LSP.SymbolKind.Enum: + return Glyph.EnumPublic; + case LSP.SymbolKind.Interface: + return Glyph.InterfacePublic; + case LSP.SymbolKind.Function: + return Glyph.DelegatePublic; + case LSP.SymbolKind.Variable: + return Glyph.Local; + case LSP.SymbolKind.Constant: + case LSP.SymbolKind.Number: + return Glyph.ConstantPublic; + case LSP.SymbolKind.String: + case LSP.SymbolKind.Boolean: + case LSP.SymbolKind.Array: + case LSP.SymbolKind.Object: + case LSP.SymbolKind.Key: + case LSP.SymbolKind.Null: + return Glyph.Local; + case LSP.SymbolKind.EnumMember: + return Glyph.EnumMemberPublic; + case LSP.SymbolKind.Struct: + return Glyph.StructurePublic; + case LSP.SymbolKind.Event: + return Glyph.EventPublic; + case LSP.SymbolKind.Operator: + return Glyph.Operator; + case LSP.SymbolKind.TypeParameter: + return Glyph.TypeParameter; + default: + return Glyph.None; + } + } + + public static LSP.SymbolKind GlyphToSymbolKind(Glyph glyph) + { + // Glyph kinds have accessibility modifiers in their name, e.g. ClassPrivate. + // Remove the accessibility modifier and try to convert to LSP symbol kind. + var glyphString = glyph.ToString().Replace(nameof(Accessibility.Public), string.Empty) + .Replace(nameof(Accessibility.Protected), string.Empty) + .Replace(nameof(Accessibility.Private), string.Empty) + .Replace(nameof(Accessibility.Internal), string.Empty); + + if (Enum.TryParse(glyphString, out var symbolKind)) + { + return symbolKind; + } + + switch (glyph) + { + case Glyph.Assembly: + case Glyph.BasicProject: + case Glyph.CSharpProject: + case Glyph.NuGet: + return LSP.SymbolKind.Package; + case Glyph.BasicFile: + case Glyph.CSharpFile: + return LSP.SymbolKind.File; + case Glyph.DelegatePublic: + case Glyph.DelegateProtected: + case Glyph.DelegatePrivate: + case Glyph.DelegateInternal: + case Glyph.ExtensionMethodPublic: + case Glyph.ExtensionMethodProtected: + case Glyph.ExtensionMethodPrivate: + case Glyph.ExtensionMethodInternal: + return LSP.SymbolKind.Method; + case Glyph.Local: + case Glyph.Parameter: + case Glyph.RangeVariable: + case Glyph.Reference: + return LSP.SymbolKind.Variable; + case Glyph.StructurePublic: + case Glyph.StructureProtected: + case Glyph.StructurePrivate: + case Glyph.StructureInternal: + return LSP.SymbolKind.Struct; + default: + return LSP.SymbolKind.Object; + } + } + + public static Glyph CompletionItemKindToGlyph(LSP.CompletionItemKind kind) + { + switch (kind) + { + case LSP.CompletionItemKind.Text: + return Glyph.None; + case LSP.CompletionItemKind.Method: + case LSP.CompletionItemKind.Constructor: + case LSP.CompletionItemKind.Function: // We don't use Function, but map it just in case. It has the same icon as Method in VS and VS Code + return Glyph.MethodPublic; + case LSP.CompletionItemKind.Field: + return Glyph.FieldPublic; + case LSP.CompletionItemKind.Variable: + case LSP.CompletionItemKind.Unit: + case LSP.CompletionItemKind.Value: + return Glyph.Local; + case LSP.CompletionItemKind.Class: + return Glyph.ClassPublic; + case LSP.CompletionItemKind.Interface: + return Glyph.InterfacePublic; + case LSP.CompletionItemKind.Module: + return Glyph.ModulePublic; + case LSP.CompletionItemKind.Property: + return Glyph.PropertyPublic; + case LSP.CompletionItemKind.Enum: + return Glyph.EnumPublic; + case LSP.CompletionItemKind.Keyword: + return Glyph.Keyword; + case LSP.CompletionItemKind.Snippet: + return Glyph.Snippet; + case LSP.CompletionItemKind.Color: + return Glyph.None; + case LSP.CompletionItemKind.File: + return Glyph.CSharpFile; + case LSP.CompletionItemKind.Reference: + return Glyph.Reference; + case LSP.CompletionItemKind.Folder: + return Glyph.OpenFolder; + case LSP.CompletionItemKind.EnumMember: + return Glyph.EnumMemberPublic; + case LSP.CompletionItemKind.Constant: + return Glyph.ConstantPublic; + case LSP.CompletionItemKind.Struct: + return Glyph.StructurePublic; + case LSP.CompletionItemKind.Event: + return Glyph.EventPublic; + case LSP.CompletionItemKind.Operator: + return Glyph.Operator; + case LSP.CompletionItemKind.TypeParameter: + return Glyph.TypeParameter; + default: + return Glyph.None; + } + } + + // The mappings here are roughly based off of SymbolUsageInfoExtensions.ToSymbolReferenceKinds. + public static LSP.VSInternalReferenceKind[] SymbolUsageInfoToReferenceKinds(SymbolUsageInfo symbolUsageInfo) + { + using var _ = ArrayBuilder.GetInstance(out var referenceKinds); + if (symbolUsageInfo.ValueUsageInfoOpt.HasValue) + { + var usageInfo = symbolUsageInfo.ValueUsageInfoOpt.Value; + if (usageInfo.IsReadFrom()) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Read); + } + + if (usageInfo.IsWrittenTo()) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Write); + } + + if (usageInfo.IsReference()) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Reference); + } + + if (usageInfo.IsNameOnly()) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Name); + } + } + + if (symbolUsageInfo.TypeOrNamespaceUsageInfoOpt.HasValue) + { + var usageInfo = symbolUsageInfo.TypeOrNamespaceUsageInfoOpt.Value; + if ((usageInfo & TypeOrNamespaceUsageInfo.Qualified) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Qualified); + } + + if ((usageInfo & TypeOrNamespaceUsageInfo.TypeArgument) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.TypeArgument); + } + + if ((usageInfo & TypeOrNamespaceUsageInfo.TypeConstraint) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.TypeConstraint); + } + + if ((usageInfo & TypeOrNamespaceUsageInfo.Base) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.BaseType); + } + + // Preserving the same mapping logic that SymbolUsageInfoExtensions.ToSymbolReferenceKinds uses + if ((usageInfo & TypeOrNamespaceUsageInfo.ObjectCreation) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Constructor); + } + + if ((usageInfo & TypeOrNamespaceUsageInfo.Import) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Import); + } + + // Preserving the same mapping logic that SymbolUsageInfoExtensions.ToSymbolReferenceKinds uses + if ((usageInfo & TypeOrNamespaceUsageInfo.NamespaceDeclaration) != 0) + { + referenceKinds.Add(LSP.VSInternalReferenceKind.Declaration); + } + } + + return referenceKinds.ToArray(); + } + + public static string ProjectIdToProjectContextId(ProjectId id) + { + return id.Id + "|" + id.DebugName; + } + + public static ProjectId ProjectContextToProjectId(LSP.VSProjectContext projectContext) + { + var delimiter = projectContext.Id.IndexOf('|'); + + return ProjectId.CreateFromSerialized( + Guid.Parse(projectContext.Id[..delimiter]), + debugName: projectContext.Id[(delimiter + 1)..]); + } + + public static LSP.VSProjectContext ProjectToProjectContext(Project project) + { + var projectContext = new LSP.VSProjectContext + { + Id = ProjectIdToProjectContextId(project.Id), + Label = project.Name + }; + + if (project.Language == LanguageNames.CSharp) + { + projectContext.Kind = LSP.VSProjectKind.CSharp; + } + else if (project.Language == LanguageNames.VisualBasic) + { + projectContext.Kind = LSP.VSProjectKind.VisualBasic; + } + + return projectContext; + } + + public static async Task GetFormattingOptionsAsync( + LSP.FormattingOptions? options, + Document document, + IGlobalOptionService globalOptions, + CancellationToken cancellationToken) + { + var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(globalOptions, cancellationToken).ConfigureAwait(false); + + if (options != null) + { + // LSP doesn't currently support indent size as an option. However, except in special + // circumstances, indent size is usually equivalent to tab size, so we'll just set it. + formattingOptions = formattingOptions with + { + LineFormatting = new() + { + UseTabs = !options.InsertSpaces, + TabSize = options.TabSize, + IndentationSize = options.TabSize, + NewLine = formattingOptions.NewLine + } + }; + } + + return formattingOptions; + } + + public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray tags, TextDocument document, bool featureSupportsMarkdown) + => GetDocumentationMarkupContent(tags, document.Project.Language, featureSupportsMarkdown); + + public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray tags, string language, bool featureSupportsMarkdown) + { + if (!featureSupportsMarkdown) + { + return new LSP.MarkupContent + { + Kind = LSP.MarkupKind.PlainText, + Value = tags.GetFullText(), + }; + } + + using var markdownBuilder = new MarkdownContentBuilder(); + string? codeFence = null; + foreach (var taggedText in tags) + { + switch (taggedText.Tag) + { + case TextTags.CodeBlockStart: + if (markdownBuilder.IsLineEmpty()) + { + // If the current line is empty, we can append a code block. + codeFence = BlockCodeFence; + var codeBlockLanguageName = GetCodeBlockLanguageName(language); + markdownBuilder.AppendLine($"{codeFence}{codeBlockLanguageName}"); + markdownBuilder.AppendLine(taggedText.Text); + } + else + { + // There is text on the line already - we should append an in-line code block. + codeFence = InlineCodeFence; + markdownBuilder.Append(codeFence + taggedText.Text); + } + break; + case TextTags.CodeBlockEnd: + if (codeFence == BlockCodeFence) + { + markdownBuilder.AppendLine(codeFence); + markdownBuilder.AppendLine(taggedText.Text); + } + else if (codeFence == InlineCodeFence) + { + markdownBuilder.Append(codeFence + taggedText.Text); + } + else + { + throw ExceptionUtilities.UnexpectedValue(codeFence); + } + + codeFence = null; + + break; + case TextTags.LineBreak: + // A line ending with double space and a new line indicates to markdown + // to render a single-spaced line break. + markdownBuilder.Append(" "); + markdownBuilder.AppendLine(); + break; + default: + var styledText = GetStyledText(taggedText, codeFence != null); + markdownBuilder.Append(styledText); + break; + } + } + + var content = markdownBuilder.Build(Environment.NewLine); + + return new LSP.MarkupContent + { + Kind = LSP.MarkupKind.Markdown, + Value = content, + }; + + static string GetCodeBlockLanguageName(string language) + { + return language switch + { + (LanguageNames.CSharp) => CSharpMarkdownLanguageName, + (LanguageNames.VisualBasic) => VisualBasicMarkdownLanguageName, + _ => throw new InvalidOperationException($"{language} is not supported"), + }; + } + + static string GetStyledText(TaggedText taggedText, bool isInCodeBlock) + { + var isCode = isInCodeBlock || taggedText.Style is TaggedTextStyle.Code; + var text = isCode ? taggedText.Text : s_markdownEscapeRegex.Replace(taggedText.Text, @"\$1"); + + // For non-cref links, the URI is present in both the hint and target. + if (!string.IsNullOrEmpty(taggedText.NavigationHint) && taggedText.NavigationHint == taggedText.NavigationTarget) + return $"[{text}]({taggedText.NavigationHint})"; + + // Markdown ignores spaces at the start of lines outside of code blocks, + // so we replace regular spaces with non-breaking spaces to ensure structural space is retained. + // We want to use regular spaces everywhere else to allow the client to wrap long text. + if (!isCode && taggedText.Tag is TextTags.Space or TextTags.ContainerStart) + text = text.Replace(" ", " "); + + return taggedText.Style switch + { + TaggedTextStyle.None => text, + TaggedTextStyle.Strong => $"**{text}**", + TaggedTextStyle.Emphasis => $"_{text}_", + TaggedTextStyle.Underline => $"{text}", + TaggedTextStyle.Code => $"`{text}`", + _ => text, + }; + } + } + + private static async Task?> GetMappedSpanResultAsync(TextDocument textDocument, ImmutableArray textSpans, CancellationToken cancellationToken) + { + if (textDocument is not Document document) + { + return null; + } + + var spanMappingService = document.Services.GetService(); + if (spanMappingService == null) + { + return null; + } + + var mappedSpanResult = await spanMappingService.MapSpansAsync(document, textSpans, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfFalse(textSpans.Length == mappedSpanResult.Length, + $"The number of input spans {textSpans.Length} should match the number of mapped spans returned {mappedSpanResult.Length}"); + return mappedSpanResult; + } + + private static LSP.Range MappedSpanResultToRange(MappedSpanResult mappedSpanResult) + { + return new LSP.Range + { + Start = LinePositionToPosition(mappedSpanResult.LinePositionSpan.Start), + End = LinePositionToPosition(mappedSpanResult.LinePositionSpan.End) + }; + } + */ + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/RequestContext.cs b/MSBuildLanguageServer/Import/RequestContext.cs new file mode 100644 index 00000000..77a08ff2 --- /dev/null +++ b/MSBuildLanguageServer/Import/RequestContext.cs @@ -0,0 +1,428 @@ +// based on +// https://raw.githubusercontent.com/dotnet/roslyn/b9c35f1021d2d9d40521474c388d49ee7580ec98/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs +// portions commented out using /* */ comments +// and other changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler; + +/// +/// Context for requests handled by +/// +internal readonly struct RequestContext +{ + /// + /// This will be the for non-mutating requests because they're not allowed to change documents + /// + private readonly IDocumentChangeTracker _documentChangeTracker; + + /// + /// The client capabilities for the request. + /// + /// + /// Should only be null on the "initialize" request. + /// + private readonly ClientCapabilities? _clientCapabilities; + + /// + /// Contains the LSP text for all opened LSP documents from when this request was processed in the queue. + /// + /// + /// This is a snapshot of the source text that reflects the LSP text based on the order of this request in the queue. + /// It contains text that is consistent with all prior LSP text sync notifications, but LSP text sync requests + /// which are ordered after this one in the queue are not reflected here. + /// + private readonly ImmutableDictionary _trackedDocuments; + + private readonly ILspServices _lspServices; + + /* + /// + /// Provides backing storage for the LSP workspace used by this RequestContext instance, allowing it to be cleared + /// on demand from all copies that may exist of this value type. + /// + /// + /// This field is only initialized for handlers that request solution context. + /// + private readonly StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>? _lspSolution; + + /// + /// The workspace this request is for, if applicable. This will be present if is + /// present. It will be if requiresLSPSolution is false. + /// + public Workspace? Workspace + { + get { + if(_lspSolution is null) + { + // This request context never had a workspace instance + return null; + } + + // The workspace is available unless it has been cleared by a call to ClearSolutionContext. Explicitly throw + // for attempts to access this property after it has been manually cleared. + return _lspSolution.Value.Workspace ?? throw new InvalidOperationException(); + } + } + + /// + /// The solution state that the request should operate on, if the handler requires an LSP solution, or otherwise + /// + public Solution? Solution + { + get { + if(_lspSolution is null) + { + // This request context never had a solution instance + return null; + } + + // The solution is available unless it has been cleared by a call to ClearSolutionContext. Explicitly throw + // for attempts to access this property after it has been manually cleared. + return _lspSolution.Value.Solution ?? throw new InvalidOperationException(); + } + } + + /// + /// The document that the request is for, if applicable. This comes from the returned from the handler itself via a call to + /// . + /// + public Document? Document + { + get { + if(this.TextDocument is null) + { + return null; + } + + if(this.TextDocument is Document document) + { + return document; + } + + // Explicitly throw for attempts to get a Document when only a TextDocument is available. + throw new InvalidOperationException("Attempted to retrieve a Document but a TextDocument was found instead."); + } + } + + /// + /// The text document that the request is for, if applicable. This comes from the returned from the handler itself via a call to + /// . + /// + public TextDocument? TextDocument + { + get { + if(_lspSolution is null) + { + // This request context never had a solution instance + return null; + } + + // The solution is available unless it has been cleared by a call to ClearSolutionContext. Explicitly throw + // for attempts to access this property after it has been manually cleared. Note that we can't rely on + // Document being null for this check, because it is not always provided as part of the solution context. + if(_lspSolution.Value.Workspace is null) + { + throw new InvalidOperationException(); + } + + return _lspSolution.Value.Document; + } + } + */ + + /// + /// The LSP server handling the request. + /// + public readonly WellKnownLspServerKinds ServerKind; + + /// + /// The method this request is targeting. + /// + public readonly string Method; + + /// + /// The languages supported by the server making the request. + /// + public readonly ImmutableArray SupportedLanguages; + + public readonly CancellationToken QueueCancellationToken; + + /// + /// Tracing object that can be used to log information about the status of requests. + /// + private readonly ILspLogger _logger; + + public RequestContext( + /* + Workspace? workspace, + Solution? solution, + */ + ILspLogger logger, + string method, + ClientCapabilities? clientCapabilities, + WellKnownLspServerKinds serverKind, + // MODOFICATION: changed from TextDocument to LspEditorDocument + LspEditorDocument? document, + IDocumentChangeTracker documentChangeTracker, + ImmutableDictionary trackedDocuments, + ImmutableArray supportedLanguages, + ILspServices lspServices, + CancellationToken queueCancellationToken) + { + /* + if(workspace is not null) + { + RoslynDebug.Assert(solution is not null); + _lspSolution = new StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>((workspace, solution, document)); + } else + { + RoslynDebug.Assert(solution is null); + RoslynDebug.Assert(document is null); + _lspSolution = null; + } + */ + + // MODIFICATION: new code + this.document = document; + // END MODIFICATION + + _clientCapabilities = clientCapabilities; + ServerKind = serverKind; + SupportedLanguages = supportedLanguages; + _documentChangeTracker = documentChangeTracker; + _logger = logger; + _trackedDocuments = trackedDocuments; + _lspServices = lspServices; + QueueCancellationToken = queueCancellationToken; + Method = method; + } + + public ClientCapabilities GetRequiredClientCapabilities() + { + return _clientCapabilities is null + ? throw new ArgumentNullException($"{nameof(ClientCapabilities)} is null when it was required for {Method}") + : _clientCapabilities; + } + + // MODIFICATION new code based on GetRequiredDocument + readonly LspEditorDocument? document; + + public LspEditorDocument? Document => document; + + public LspEditorDocument GetRequiredDocument() => document ?? throw new ArgumentNullException($"{nameof(Document)} is null when it was required for {Method}"); + // END MODIFCATION + + /* + public Document GetRequiredDocument() + { + return Document is null + ? throw new ArgumentNullException($"{nameof(Document)} is null when it was required for {Method}") + : Document; + } +/* + public TextDocument GetRequiredTextDocument() + { + return TextDocument is null + ? throw new ArgumentNullException($"{nameof(TextDocument)} is null when it was required for {Method}") + : TextDocument; + } + */ + + public static async Task CreateAsync( + bool mutatesSolutionState, + bool requiresLSPSolution, + TextDocumentIdentifier? textDocument, + WellKnownLspServerKinds serverKind, + ClientCapabilities? clientCapabilities, + ImmutableArray supportedLanguages, + ILspServices lspServices, + ILspLogger logger, + string method, + CancellationToken cancellationToken) + { + // MODIFICATION: changed LspWorkspaceManager to LspEditorWorkspace + var lspWorkspaceManager = lspServices.GetRequiredService(); + var documentChangeTracker = mutatesSolutionState ? (IDocumentChangeTracker)lspWorkspaceManager : new NonMutatingDocumentChangeTracker(); + + // Retrieve the current LSP tracked text as of this request. + // This is safe as all creation of request contexts cannot happen concurrently. + var trackedDocuments = lspWorkspaceManager.GetTrackedLspText(); + + // If the handler doesn't need an LSP solution we do two important things: + // 1. We don't bother building the LSP solution for perf reasons + // 2. We explicitly don't give the handler a solution or document, even if we could + // so they're not accidentally operating on stale solution state. + RequestContext context; + if(!requiresLSPSolution) + { + context = new RequestContext( + /*workspace: null, solution: null,*/ logger: logger, method: method, clientCapabilities: clientCapabilities, serverKind: serverKind, document: null, + documentChangeTracker: documentChangeTracker, trackedDocuments: trackedDocuments, supportedLanguages: supportedLanguages, lspServices: lspServices, + queueCancellationToken: cancellationToken); + + } else + { + // MODIFICATION: New code + if (textDocument is null) { + throw new ArgumentNullException($"{nameof(textDocument)} is null when it was required for {method}"); + } + + var document = lspWorkspaceManager.GetEditorDocument(textDocument.Uri); + // END MODIFICATION + /* + Workspace? workspace = null; + Solution? solution = null; + TextDocument? document = null; + if(textDocument is not null) + { + // we were given a request associated with a document. Find the corresponding roslyn document for this. + // There are certain cases where we may be asked for a document that does not exist (for example a + // document is removed) For example, document pull diagnostics can ask us after removal to clear + // diagnostics for a document. + (workspace, solution, document) = await lspWorkspaceManager.GetLspDocumentInfoAsync(textDocument, cancellationToken).ConfigureAwait(false); + } + + if(workspace is null) + { + (workspace, solution) = await lspWorkspaceManager.GetLspSolutionInfoAsync(cancellationToken).ConfigureAwait(false); + } + + if(workspace is null || solution is null) + { + logger.LogError($"Could not find appropriate workspace or solution on {method}"); + FatalError.ReportWithDumpAndCatch(new Exception( + $"Could not find appropriate workspace or solution on {method}"), ErrorSeverity.Critical); + } +*/ + context = new RequestContext(/* + workspace, + solution,*/ + logger, + method, + clientCapabilities, + serverKind, + document, + documentChangeTracker, + trackedDocuments, + supportedLanguages, + lspServices, + cancellationToken); + } + + return context; + } + + /// + /// Allows a mutating request to open a document and start it being tracked. + /// Mutating requests are serialized by the execution queue in order to prevent concurrent access. + /// + public ValueTask StartTrackingAsync(Uri uri, SourceText initialText, string languageId, CancellationToken cancellationToken) + => _documentChangeTracker.StartTrackingAsync(uri, initialText, languageId, cancellationToken); + + /// + /// Allows a mutating request to update the contents of a tracked document. + /// Mutating requests are serialized by the execution queue in order to prevent concurrent access. + /// + public void UpdateTrackedDocument(Uri uri, SourceText changedText) + => _documentChangeTracker.UpdateTrackedDocument(uri, changedText); + + public SourceText GetTrackedDocumentSourceText(Uri documentUri) + { + Contract.ThrowIfFalse(_trackedDocuments.ContainsKey(documentUri), $"Attempted to get text for {documentUri} which is not open."); + return _trackedDocuments[documentUri].Text; + } + + /* + public TDocument? GetTrackedDocument() where TDocument : TextDocument + { + // Note: context.Document may be null in the case where the client is asking about a document that we have + // since removed from the workspace. In this case, we don't really have anything to process. + // GetPreviousResults will be used to properly realize this and notify the client that the doc is gone. + // + // Only consider open documents here (and only closed ones in the WorkspacePullDiagnosticHandler). Each + // handler treats those as separate worlds that they are responsible for. + if(TextDocument is not TDocument document) + { + TraceInformation($"Ignoring diagnostics request because no {typeof(TDocument).Name} was provided"); + return null; + } + + if(!IsTracking(document.GetURI())) + { + TraceWarning($"Ignoring diagnostics request for untracked document: {document.GetURI()}"); + return null; + } + + return document; + } + */ + + /// + /// Allows a mutating request to close a document and stop it being tracked. + /// Mutating requests are serialized by the execution queue in order to prevent concurrent access. + /// + public ValueTask StopTrackingAsync(Uri uri, CancellationToken cancellationToken) + => _documentChangeTracker.StopTrackingAsync(uri, cancellationToken); + + public bool IsTracking(Uri documentUri) + => _trackedDocuments.ContainsKey(documentUri); + + public void ClearSolutionContext() + { + /* + if(_lspSolution is null) + return; + + _lspSolution.Value = default; + */ + } + + /// + /// Logs an informational message. + /// + public void TraceInformation(string message) + => _logger.LogInformation(message); + + public void TraceWarning(string message) + => _logger.LogWarning(message); + + public void TraceError(string message) + => _logger.LogError(message); + + public void TraceException(Exception exception) + => _logger.LogException(exception); + + public T GetRequiredLspService() where T : class, ILspService + { + return _lspServices.GetRequiredService(); + } + + public T GetRequiredService() where T : class + { + return _lspServices.GetRequiredService(); + } + + public IEnumerable GetRequiredServices() where T : class + { + return _lspServices.GetRequiredServices(); + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/Stubs.cs b/MSBuildLanguageServer/Import/Stubs.cs new file mode 100644 index 00000000..12159fa7 --- /dev/null +++ b/MSBuildLanguageServer/Import/Stubs.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// stubs to help imported files work w/o bringing in too many dependencies + +using System.Composition; +using System.Runtime.CompilerServices; + +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer +{ + enum WellKnownLspServerKinds + { + MSBuild, + Any, + + // some imported classes use this, alias it to our MSBuild value + CSharpVisualBasicLspServer = MSBuild, + + // used by AbstractLanguageServerProtocolTests + AlwaysActiveVSLspServer + } + + static class WellKnownLspServerKindExtensions + { + public static string ToTelemetryString(this WellKnownLspServerKinds serverKind) + => serverKind switch + { + WellKnownLspServerKinds.MSBuild => LanguageName.MSBuild, + _ => throw ExceptionUtilities.UnexpectedValue(serverKind), + }; + } + + class LanguageName + { + public const string MSBuild = nameof(MSBuild); + } +} + +// Logger.cs has a Using for this namespace but doesn't actually use classes from it +namespace Microsoft.CodeAnalysis.Options { +} + +namespace Microsoft.CodeAnalysis.LanguageServer +{ + interface ExperimentalCapabilitiesProvider : ICapabilitiesProvider { } +} + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler +{ + class RoslynDocumentSymbol : Roslyn.LanguageServer.Protocol.DocumentSymbol { } +} + +namespace Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions +{ + class StarredCompletionAssemblyHelper + { + // not called unless serverConfiguration.StarredCompletionsPath is non-null + public static string GetStarredCompletionAssemblyPath(string starredCompletionsPath) + => throw new NotSupportedException (); + } + +} + +namespace Microsoft.CodeAnalysis.Serialization +{ + struct AssetPath + { + public bool IncludeDocumentAttributes { get; } + public bool IncludeDocumentText { get; } + } +} + +namespace Microsoft.CodeAnalysis.Shared.TestHooks +{ + [Shared] + [Export(typeof(IAsynchronousOperationListenerProvider))] + [Export(typeof(AsynchronousOperationListenerProvider))] + internal sealed partial class AsynchronousOperationListenerProvider : IAsynchronousOperationListenerProvider + { + public static readonly IAsynchronousOperationListenerProvider NullProvider = new NullListenerProvider(); + public static readonly IAsynchronousOperationListener NullListener = new NullOperationListener(); + + internal static void Enable(bool enable, bool diagnostics) + { + } + + public IAsynchronousOperationListener GetListener(string featureName) => NullListener; + + internal IEnumerable GetTokens() => []; + + internal Task WaitAllDispatcherOperationAndTasksAsync(Workspace? workspace) => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Import/VisualStudioMefHostServices.cs b/MSBuildLanguageServer/Import/VisualStudioMefHostServices.cs new file mode 100644 index 00000000..7854eb27 --- /dev/null +++ b/MSBuildLanguageServer/Import/VisualStudioMefHostServices.cs @@ -0,0 +1,101 @@ +// modified copy of +// https://raw.githubusercontent.com/dotnet/roslyn/dd3795a49875bef2728f01e0284406f33ea1e2e1/src/Workspaces/Remote/Core/VisualStudioMefHostServices.cs +// portions commented out using /* */ comments +// and other changes annotated inline with // MODIFICATION + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host.Mef +{ + /// + /// Provides host services imported via VS MEF. + /// + internal sealed class VisualStudioMefHostServices : HostServices, IMefHostExportProvider + { + // the export provider for the MEF composition + private readonly ExportProvider _exportProvider; + + // accumulated cache for exports + private ImmutableDictionary _exportsMap + = ImmutableDictionary.Empty; + + private VisualStudioMefHostServices(ExportProvider exportProvider) + { + Contract.ThrowIfNull(exportProvider); + _exportProvider = exportProvider; + } + + public static VisualStudioMefHostServices Create(ExportProvider exportProvider) + => new(exportProvider); + + /// + /// Creates a new associated with the specified workspace. + /// + // MODIFICATION: changed "protected internal" to "protected" as we don't have IVT for HostServices + protected override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace) + => new MefWorkspaceServices(this, workspace); + + /// + /// Gets all the MEF exports of the specified type with the specified metadata. + /// + public IEnumerable> GetExports() + { + var key = new ExportKey(typeof(TExtension).AssemblyQualifiedName!, typeof(TMetadata).AssemblyQualifiedName!); + if (!_exportsMap.TryGetValue(key, out var exports)) + { + exports = ImmutableInterlocked.GetOrAdd(ref _exportsMap, key, _ => + { + return _exportProvider.GetExports().ToImmutableArray(); + }); + } + + return (IEnumerable>)exports; + } + + /// + /// Gets all the MEF exports of the specified type. + /// + public IEnumerable> GetExports() + { + var key = new ExportKey(typeof(TExtension).AssemblyQualifiedName!, ""); + if (!_exportsMap.TryGetValue(key, out var exports)) + { + exports = ImmutableInterlocked.GetOrAdd(ref _exportsMap, key, _ => + _exportProvider.GetExports().ToImmutableArray()); + } + + return (IEnumerable>)exports; + } + + private readonly struct ExportKey : IEquatable + { + internal readonly string ExtensionTypeName; + internal readonly string MetadataTypeName; + + public ExportKey(string extensionTypeName, string metadataTypeName) + { + ExtensionTypeName = extensionTypeName; + MetadataTypeName = metadataTypeName; + } + + public bool Equals(ExportKey other) + => string.Compare(ExtensionTypeName, other.ExtensionTypeName, StringComparison.OrdinalIgnoreCase) == 0 && + string.Compare(MetadataTypeName, other.MetadataTypeName, StringComparison.OrdinalIgnoreCase) == 0; + + public override bool Equals(object? obj) + => obj is ExportKey key && Equals(key); + + public override int GetHashCode() + => Hash.Combine(MetadataTypeName.GetHashCode(), ExtensionTypeName.GetHashCode()); + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/InternalsVisibleTo.cs b/MSBuildLanguageServer/InternalsVisibleTo.cs new file mode 100644 index 00000000..f939541b --- /dev/null +++ b/MSBuildLanguageServer/InternalsVisibleTo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; +using MonoDevelop.MSBuild; + +[assembly: InternalsVisibleTo ($"MSBuildLanguageServer.Tests, {IVT.PublicKeyAtt}")] \ No newline at end of file diff --git a/MSBuildLanguageServer/KnownVSCodeImages.cs b/MSBuildLanguageServer/KnownVSCodeImages.cs new file mode 100644 index 00000000..67f7ae89 --- /dev/null +++ b/MSBuildLanguageServer/KnownVSCodeImages.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable annotations + +using System.Text; + +namespace MonoDevelop.MSBuild.Editor; + +enum KnownVSCodeImages +{ + // VS Code misc + Error, + Warning, + Info, + Target, + Run, + + // VS Code symbols + SymbolBoolean, + SymbolClass, + SymbolColor, + SymbolConstant, + SymbolConstructor, + SymbolEnum, + SymbolEnumMember, + SymbolEvent, + SymbolField, + SymbolFile, + SymbolFolder, + SymbolFunction, + SymbolInterface, + SymbolKey, + SymbolKeyword, + SymbolMethod, + SymbolMisc, + SymbolModule, + SymbolNamespace, + SymbolNull, + SymbolNumber, + SymbolNumeric, + SymbolObject, + SymbolOperator, + SymbolPackage, + SymbolParameter, + SymbolProperty, + SymbolReference, + SymbolRuler, + SymbolSnippet, + SymbolString, + SymbolStruct, + SymbolStructure, + SymbolText, + SymbolTypeParameter, + SymbolUnit, + SymbolValue, + SymbolVariable, +} + +static class KnownImagesExtensions +{ + public static string ToVSCodeImageId(this KnownVSCodeImages knownImage) + { + var name = knownImage.ToString(); + var sb = new StringBuilder(name.Length + 3); + for(int i = 0; i < name.Length; i++) + { + char c = name[i]; + if(char.IsAsciiLetterUpper(c)) + { + c = (char)(c + ('a' - 'A')); + if(i > 0) + { + sb.Append('-'); + } + } + sb.Append(c); + } + return sb.ToString(); + } + + public static KnownVSCodeImages ToVSCodeImage(this MSBuildGlyph glyph) + => glyph switch { + MSBuildGlyph.MSBuildKeyword => KnownVSCodeImages.SymbolKeyword, + MSBuildGlyph.MSBuildItem => KnownVSCodeImages.SymbolObject, + MSBuildGlyph.MSBuildProperty => KnownVSCodeImages.SymbolProperty, + MSBuildGlyph.MSBuildTarget => KnownVSCodeImages.Target, + MSBuildGlyph.MSBuildMetadata => KnownVSCodeImages.SymbolField, + MSBuildGlyph.MSBuildTask => KnownVSCodeImages.Run, + MSBuildGlyph.MSBuildTaskParameter => KnownVSCodeImages.SymbolParameter, + MSBuildGlyph.MSBuildConstant => KnownVSCodeImages.SymbolConstant, + MSBuildGlyph.MSBuildSdk => KnownVSCodeImages.SymbolFolder, + MSBuildGlyph.MSBuildFrameworkId => KnownVSCodeImages.SymbolEnum, + MSBuildGlyph.Deprecated => KnownVSCodeImages.Warning, + MSBuildGlyph.Warning => KnownVSCodeImages.Warning, + MSBuildGlyph.Information => KnownVSCodeImages.Info, + MSBuildGlyph.Error => KnownVSCodeImages.Error, + MSBuildGlyph.Folder => KnownVSCodeImages.SymbolFolder, + MSBuildGlyph.File => KnownVSCodeImages.SymbolFile, + MSBuildGlyph.DotNetProperty => KnownVSCodeImages.SymbolProperty, + MSBuildGlyph.DotNetMethod => KnownVSCodeImages.SymbolMethod, + MSBuildGlyph.DotNetClass => KnownVSCodeImages.SymbolClass, + _ => throw new ArgumentException($"Unknown glyph '{glyph}'") + }; +} \ No newline at end of file diff --git a/MSBuildLanguageServer/LspLoggerExtensions.cs b/MSBuildLanguageServer/LspLoggerExtensions.cs new file mode 100644 index 00000000..62bc15bd --- /dev/null +++ b/MSBuildLanguageServer/LspLoggerExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +static class LspLoggerExtensions +{ + public static ILogger ToILogger(this ILspLogger lspLogger) => new LspLoggerWrapper (lspLogger); + + class LspLoggerWrapper(ILspLogger lspLogger) : ILogger + { + readonly ILspLogger lspLogger = lspLogger; + + public IDisposable? BeginScope(TState state) where TState : notnull + { + if (state.ToString () is string message) { + lspLogger.LogStartContext (message); + return new LogState (this, message); + } + return null; + } + + public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (exception is not null) { + lspLogger.LogException (exception, formatter (state, exception)); + return; + } + + switch(logLevel) { + case LogLevel.Information: + lspLogger.LogInformation (formatter (state, exception)); + break; + case LogLevel.Warning: + lspLogger.LogWarning (formatter (state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + lspLogger.LogError (formatter (state, exception)); + break; + } + } + + readonly struct LogState(LspLoggerWrapper logger, string message) : IDisposable + { + public void Dispose() + { + logger.lspLogger.LogEndContext (message); + } + } + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/MSBuildCapabilitiesProvider.cs b/MSBuildLanguageServer/MSBuildCapabilitiesProvider.cs new file mode 100644 index 00000000..5866176a --- /dev/null +++ b/MSBuildLanguageServer/MSBuildCapabilitiesProvider.cs @@ -0,0 +1,42 @@ + +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer; +using Roslyn.LanguageServer.Protocol; + +// exporting this as ExperimentalCapabilitiesProvider is required for LanguageServerHost to pick it up + +[Export(typeof(ExperimentalCapabilitiesProvider)), Shared] +class MSBuildCapabilitiesProvider : ExperimentalCapabilitiesProvider +{ + public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + var capabilities = new ServerCapabilities { + HoverProvider = true, + TextDocumentSync = new TextDocumentSyncOptions { + OpenClose = true, + Change = TextDocumentSyncKind.Incremental + // Save = true, // todo update mtime + }, + CompletionProvider = new CompletionOptions { + TriggerCharacters = [ + // xml tag + "<", + // attribute quotes + "\"", + "'", + // expressions + "(", + // -> transforms and element values + ">", + // members in property functions + "." + ], + ResolveProvider = true + }, + DiagnosticOptions = new DiagnosticOptions { }, + DefinitionProvider = new DefinitionOptions { WorkDoneProgress = true }, + ReferencesProvider = new ReferenceOptions { WorkDoneProgress = true } + }; + return capabilities; + } +} \ No newline at end of file diff --git a/MSBuildLanguageServer/MSBuildGlyph.cs b/MSBuildLanguageServer/MSBuildGlyph.cs new file mode 100644 index 00000000..461fa482 --- /dev/null +++ b/MSBuildLanguageServer/MSBuildGlyph.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable annotations + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Syntax; +using MonoDevelop.MSBuild.Language.Typesystem; + +namespace MonoDevelop.MSBuild.Editor; + +enum MSBuildGlyph +{ + MSBuildKeyword, + MSBuildItem, + MSBuildProperty, + MSBuildTarget, + MSBuildMetadata, + MSBuildTask, + MSBuildTaskParameter, + MSBuildConstant, + MSBuildSdk, + MSBuildFrameworkId, + Deprecated, + Information, + Warning, + Error, + Folder, + File, + DotNetProperty, + DotNetMethod, + DotNetClass, +} + +static class MSBuildGlyphExtensions +{ + public static MSBuildGlyph? GetGlyph(this ISymbol info, bool isPrivate) + { + switch(info) + { + case MSBuildElementSyntax el: + if(!el.IsAbstract) + return MSBuildGlyph.MSBuildKeyword; + break; + case MSBuildAttributeSyntax att: + if(!att.IsAbstract) + { + return MSBuildGlyph.MSBuildKeyword; + } + break; + case ItemInfo: + return MSBuildGlyph.MSBuildItem; + case PropertyInfo: + return MSBuildGlyph.MSBuildProperty; + case TargetInfo: + return MSBuildGlyph.MSBuildTarget; + case MetadataInfo: + return MSBuildGlyph.MSBuildMetadata; + case TaskInfo: + return MSBuildGlyph.MSBuildTask; + case ConstantSymbol: + return MSBuildGlyph.MSBuildConstant; + case FileOrFolderInfo value: + return value.IsFolder ? MSBuildGlyph.Folder : MSBuildGlyph.File; + case FrameworkInfo: + return MSBuildGlyph.MSBuildFrameworkId; + case TaskParameterInfo: + return MSBuildGlyph.MSBuildTaskParameter; + case FunctionInfo fi: + if(fi.IsProperty) + { + //FIXME: can we resolve the msbuild / .net property terminology overloading? + return MSBuildGlyph.DotNetProperty; + } + return MSBuildGlyph.DotNetMethod; + case ClassInfo _: + return MSBuildGlyph.DotNetClass; + } + return null; + } + + public static MSBuildGlyph ToGlyph(this MSBuildDiagnosticSeverity severity) + => severity switch { + MSBuildDiagnosticSeverity.Error => MSBuildGlyph.Error, + MSBuildDiagnosticSeverity.Warning => MSBuildGlyph.Warning, + _ => MSBuildGlyph.Information, + }; +} + + diff --git a/MSBuildLanguageServer/MSBuildLanguageServer.csproj b/MSBuildLanguageServer/MSBuildLanguageServer.csproj new file mode 100644 index 00000000..aa44e880 --- /dev/null +++ b/MSBuildLanguageServer/MSBuildLanguageServer.csproj @@ -0,0 +1,171 @@ + + + + net8.0 + enable + enable + MonoDevelop.MSBuild.Editor.LanguageServer + true + Exe + + $(DefineConstants);CODE_STYLE + + $(NoWarn); + + VSTHRD003;VSTHRD103;VSTHRD110;VSTHRD002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MSBuildLanguageServer/MSBuildLanguageServerFactory.cs b/MSBuildLanguageServer/MSBuildLanguageServerFactory.cs new file mode 100644 index 00000000..af09864e --- /dev/null +++ b/MSBuildLanguageServer/MSBuildLanguageServerFactory.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using System.Text.Json; + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.Composition; + +using StreamJsonRpc; + +[Export(typeof(ILanguageServerFactory)), Shared] +[method: ImportingConstructor] +class MSBuildLanguageServerFactory (CSharpVisualBasicLspServiceProvider lspServiceProvider) : ILanguageServerFactory +{ + readonly AbstractLspServiceProvider lspServiceProvider = lspServiceProvider; + + public AbstractLanguageServer Create ( + JsonRpc jsonRpc, + JsonSerializerOptions options, + ICapabilitiesProvider capabilitiesProvider, + WellKnownLspServerKinds serverKind, + AbstractLspLogger logger, + HostServices hostServices, + AbstractTypeRefResolver? typeRefResolver = null) + { + return new Microsoft.CodeAnalysis.LanguageServer.MSBuildLanguageServer ( + lspServiceProvider, + jsonRpc, + options, + capabilitiesProvider, + logger, + hostServices, + [ "MSBuild" ], + serverKind, + typeRefResolver); + } +} diff --git a/MSBuildLanguageServer/MSBuildWorkspace.cs.WIP b/MSBuildLanguageServer/MSBuildWorkspace.cs.WIP new file mode 100644 index 00000000..d4042119 --- /dev/null +++ b/MSBuildLanguageServer/MSBuildWorkspace.cs.WIP @@ -0,0 +1,80 @@ +using Microsoft.CodeAnalysis; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Schema; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +class MSBuildWorkspace +{ + // msbuild workspace directs files into solutions/projects + // solution represents projects that are grouped together for find references etc + + // generic processor that processes open documents + + // xml processor + +} + +/// +/// Represents an MSBuild project or entrypoint and the files it transitively imports. +/// The imported files may be part of more than one project. +/// +class MSBuildProject +{ + +} + +/// +/// A group of related projects considered together for find references, renames, etc. +/// A project may only be part of one ProjectGroup. +/// +class MSBuildProjectGroup +{ + +} + +/// +/// Subscribes to change events from MSBuildOpenDocumentTracker (for now), +/// runs analyzers, and fires events when diagnostics are updated +/// +class MSBuildAnalyzerService +{ + +} + + +/// +/// Tracks open documents and fires events when they change +/// +class MSBuildOpenDocumentTracker +{ + Dictionary openDocuments = new(); + + public void OpenDocument(string filePath) + { + + } + // open documents + // open document + // close document + // update document text +} + +/// +/// Immutable representation of an MSBuild document state at some point in time. +/// ALWAYS HAS A PATH: we are discarding the ability to work on unsaved files for now as it complicates things, can add it back later if needed. +/// +record class MSBuildDocumentState(DocumentId Id, MSBuildDocumentPath FilePath, VersionStamp Version, MSBuildSchema Schema, MSBuildInferredSchema InferredSchema, MSBuildDocumentImport[] Imports) +{ +} + +record class MSBuildDocumentImport(MSBuildDocumentPath FilePath) +{ + // TODO: add conditions +} + +// represents a file path that has already been made absolute and normalized +struct MSBuildDocumentPath +{ +} \ No newline at end of file diff --git a/MSBuildLanguageServer/NuGetSearchExports.cs b/MSBuildLanguageServer/NuGetSearchExports.cs new file mode 100644 index 00000000..b6d6490d --- /dev/null +++ b/MSBuildLanguageServer/NuGetSearchExports.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +using Microsoft.CodeAnalysis.LanguageServer; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; +using ProjectFileTools.NuGetSearch.Feeds.Disk; +using ProjectFileTools.NuGetSearch.Feeds.Web; +using ProjectFileTools.NuGetSearch.IO; +using ProjectFileTools.NuGetSearch.Search; + +namespace MonoDevelop.MSBuild.Editor.NuGetSearch; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(NuGetSearchService)), Shared] +[method: ImportingConstructor] +class NuGetSearchServiceFactory(IPackageSearchManager packageSearchManager) : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) => new NuGetSearchService (packageSearchManager); +} + +class NuGetSearchService(IPackageSearchManager packageSearchManager) : ILspService, IPackageSearchManager +{ + public IPackageFeedSearchJob SearchPackageInfo(string packageId, string? version, string? tfm) => packageSearchManager.SearchPackageInfo(packageId, version, tfm); + public IPackageFeedSearchJob> SearchPackageNames(string prefix, string? tfm, string? packageType = null) => packageSearchManager.SearchPackageNames(prefix, tfm, packageType); + public IPackageFeedSearchJob> SearchPackageVersions(string packageName, string? tfm, string? packageType = null) => packageSearchManager.SearchPackageVersions(packageName, tfm, packageType); +} + +[Export(typeof(IPackageFeedFactorySelector))] +[method: ImportingConstructor] +internal class ExportedPackageFeedFactorySelector([ImportMany] IEnumerable feedFactories) : PackageFeedFactorySelector(feedFactories) +{ +} + +[Export(typeof(IFileSystem))] +internal class ExportedFileSystem : FileSystem +{ +} + +[Export(typeof(IPackageFeedFactory))] +[method: ImportingConstructor] +internal class ExportedNuGetDiskFeedFactory(IFileSystem fileSystem) : NuGetDiskFeedFactory(fileSystem) +{ +} + +[Export(typeof(IPackageFeedFactory))] +[method: ImportingConstructor] +internal class ExportedNuGetV3ServiceFeedFactory(IWebRequestFactory webRequestFactory) : NuGetV3ServiceFeedFactory(webRequestFactory) +{ +} + +[Export(typeof(IPackageSearchManager))] +[method: ImportingConstructor] +internal class ExportedPackageSearchManager(IPackageFeedRegistryProvider feedRegistry, IPackageFeedFactorySelector factorySelector) : PackageSearchManager(feedRegistry, factorySelector) +{ +} + +[Export(typeof(IWebRequestFactory))] +internal class ExportedWebRequestFactory : WebRequestFactory +{ +} + +[Export(typeof(IPackageFeedRegistryProvider))] +internal class ExportedPackageFeedRegistryProvider : IPackageFeedRegistryProvider +{ + // TODO: where should we get this from? hardcode some basic ones for now + public IReadOnlyList ConfiguredFeeds { get; } = [ + "https://api.nuget.org/v3/index.json", + Environment.ExpandEnvironmentVariables("%USERPROFILE%\\.nuget\\packages") + ]; +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Parser/LspMSBuildParser.cs b/MSBuildLanguageServer/Parser/LspMSBuildParser.cs new file mode 100644 index 00000000..c80de8ad --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspMSBuildParser.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.LanguageServer; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.Xml.Editor.Parsing; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +partial class LspMSBuildParserService +{ + + // based on MonoDevelop.MSBuild.Editor.Completion.MSBuildBackgroundParser + class LspMSBuildParser : BackgroundProcessor, ILspService + { + //readonly MSBuildAnalyzerDriver analyzerDriver; + readonly LspMSBuildParserService service; + + public LspMSBuildParser(LspMSBuildParserService service, DocumentId documentId) + : base(NullBackgroundParseService.Instance) + { + this.service = service; + service.xmlParserService.SubscribeParseNotification(documentId, OnXmlParse); + /* + + analyzerDriver = new MSBuildAnalyzerDriver (this.logger);*/ + } + + void OnXmlParse(XmlParseResult result) => StartProcessing(result); + + protected override Task StartOperationAsync( + XmlParseResult input, + MSBuildParseResult? previousOutput, + XmlParseResult? previousInput, + CancellationToken token) + { + return Task.Run(() => { + var oldDoc = previousOutput?.MSBuildDocument; + + MSBuildRootDocument doc; + try + { + doc = MSBuildRootDocument.Parse( + input.Text.GetTextSource(), + input.FilePath, + oldDoc, + service.schemaProvider, + service.msbuildEnvironment, + service.taskMetadataBuilder, + service.extLogger, + token); + } catch(Exception ex) when(!(ex is OperationCanceledException && token.IsCancellationRequested)) + { + LogUnhandledParserError(ex); + doc = MSBuildRootDocument.Empty; + } + + return new MSBuildParseResult(doc, input); + }, token); + } + + void LogUnhandledParserError(Exception ex) + => service.logger.LogException(ex, "Unhandled error in MSBuild parser"); + + protected override void OnOperationCompleted(XmlParseResult input, MSBuildParseResult output) + { + service.OnParseCompleted(output); + ParseCompleted?.Invoke(this, new ParseCompletedEventArgs(output.DocumentId, output)); + } + + protected override int CompareInputs(XmlParseResult a, XmlParseResult b) + => a.Version.CompareTo(b.Version); + + public event EventHandler>? ParseCompleted; + } +} + +record MSBuildParseResult(MSBuildRootDocument MSBuildDocument, XmlParseResult XmlParseResult) +{ + public DocumentId DocumentId => XmlParseResult.DocumentState.Id; +} + +class ParseCompletedEventArgs(DocumentId documentId, TParseResult result) : EventArgs +{ + public DocumentId DocumentId { get; } = documentId; + public TParseResult Result { get; } = result; +} diff --git a/MSBuildLanguageServer/Parser/LspMSBuildParserService.cs b/MSBuildLanguageServer/Parser/LspMSBuildParserService.cs new file mode 100644 index 00000000..1d40fbfe --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspMSBuildParserService.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Schema; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +partial class LspMSBuildParserService : ILspService +{ + readonly ILspLogger logger; + readonly ILogger extLogger; + readonly LspEditorWorkspace workspace; + readonly LspXmlParserService xmlParserService; + readonly IMSBuildEnvironment msbuildEnvironment; + readonly MSBuildSchemaProvider schemaProvider; + readonly ITaskMetadataBuilder taskMetadataBuilder; + + Dictionary parsers = new(); + + public LspMSBuildParserService( + ILspLogger logger, + LspEditorWorkspace workspace, + LspXmlParserService xmlParserService, + IMSBuildEnvironment msbuildEnvironment, + MSBuildSchemaProvider schemaProvider, + ITaskMetadataBuilder taskMetadataBuilder) + { + this.logger = logger; + this.extLogger = logger.ToILogger(); + this.workspace = workspace; + this.xmlParserService = xmlParserService; + this.msbuildEnvironment = msbuildEnvironment; + this.schemaProvider = schemaProvider; + this.taskMetadataBuilder = taskMetadataBuilder; + + // as this service takes an LspXmlParserService argument, the XmlParserService will register its workspace events before this does + // so by the time our OnDocumentOpened fires, the XmlParserService's OnDocumentOpened will have fired already + workspace.DocumentOpened += OnDocumentOpened; + workspace.DocumentClosed += OnDocumentClosed; + + foreach(var openDoc in workspace.OpenDocuments) + { + OnDocumentOpened(openDoc.CurrentState); + } + } + void OnDocumentOpened(object? sender, EditorDocumentEventArgs e) => OnDocumentOpened(e.Document); + + void OnDocumentOpened(EditorDocumentState document) + { + var parser = new LspMSBuildParser(this, document.Id); + parsers.Add(document.Id, parser); + } + + void OnDocumentClosed(object? sender, EditorDocumentEventArgs e) + { + if(parsers.Remove(e.DocumentId, out var parser)) + { + parser.Dispose(); + } + } + + /// Gets a task representing a parse result for the specified document state. It may be completed or running. + /// Returns false if the document was closed + public bool TryGetParseResult(EditorDocumentState document, [NotNullWhen(true)] out Task? task, CancellationToken cancellationToken = default) + { + if(parsers.TryGetValue(document.Id, out var parser)) + { + if (!xmlParserService.TryGetParseResult(document, out Task? xmlTask, cancellationToken)) + { + task = null; + return false; + } + task = xmlTask.ContinueWith(xt => parser.GetOrProcessAsync(xt.Result, cancellationToken), cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default).Unwrap(); + return true; + } + + task = null; + return false; + } + + /// Gets the last completed parse result for the specified document. It may be newer than the specified document state. + /// Returns false if the document has not parsed successfully or if the document was closed + public bool TryGetCompletedParseResult(EditorDocumentState document, [NotNullWhen(true)] out MSBuildParseResult? lastSuccessfulResult) + { + if(parsers.TryGetValue(document.Id, out var parser)) + { + lastSuccessfulResult = parser.LastOutput; + return lastSuccessfulResult is not null; + } + + lastSuccessfulResult = null; + return false; + } + + public void SubscribeParseNotification(DocumentId documentId, Action handler) + { + var parser = parsers[documentId]; + parser.ParseCompleted += (e, a) => handler(a.Result); + + if(parser.LastOutput is { } result) + { + handler(result); + } + } + + internal void OnParseCompleted(MSBuildParseResult result) + { + ParseCompleted?.Invoke(this, new ParseCompletedEventArgs(result.DocumentId, result)); + } + + public event EventHandler>? ParseCompleted; +} diff --git a/MSBuildLanguageServer/Parser/LspMSBuildParserServiceFactory.cs b/MSBuildLanguageServer/Parser/LspMSBuildParserServiceFactory.cs new file mode 100644 index 00000000..b250e71a --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspMSBuildParserServiceFactory.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Services; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Schema; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Parser; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(LspMSBuildParserService)), Shared] +class LspMSBuildParserFactory : ILspServiceFactory +{ + ITaskMetadataBuilder taskMetadataBuilder; + MSBuildSchemaProvider schemaProvider; + + [ImportingConstructor] + public LspMSBuildParserFactory( + [Import(AllowDefault = true)] ITaskMetadataBuilder taskMetadataBuilder, + [Import(AllowDefault = true)] MSBuildSchemaProvider schemaProvider) + { + this.taskMetadataBuilder = taskMetadataBuilder ?? new NoopTaskMetadataBuilder(); + this.schemaProvider = schemaProvider ?? new MSBuildSchemaProvider(); + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + var workspace = lspServices.GetRequiredService(); + var xmlParserService = lspServices.GetRequiredService(); + var runtimeService = lspServices.GetRequiredService(); + var msbuildEnvironment = runtimeService.MSBuildEnvironment; + + return new LspMSBuildParserService(logger, workspace, xmlParserService, msbuildEnvironment, schemaProvider, taskMetadataBuilder); + } +} diff --git a/MSBuildLanguageServer/Parser/LspTextExtensions.cs b/MSBuildLanguageServer/Parser/LspTextExtensions.cs new file mode 100644 index 00000000..1b144a11 --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspTextExtensions.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +using MonoDevelop.Xml.Parser; + +using BF = System.Reflection.BindingFlags; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Parser; + +static class LspTextExtensions +{ + public static int CompareTo(this VersionStamp version, VersionStamp other) + { + if(version == other) + { + return 0; + } + + // FIXME: is there a better way to do this? + var versionStampType = typeof(VersionStamp); + var testAccessorType = versionStampType.GetNestedType("TestAccessor", BF.NonPublic) + ?? throw new InvalidOperationException("Could not find VersionStamp.TestAccessor type"); + var getTestAccessor = versionStampType.GetMethod("GetTestAccessor", BF.NonPublic | BF.Instance) + ?? throw new InvalidOperationException("Could not find VersionStamp.GetTestAccessor method"); + var isNewerThanMethod = testAccessorType.GetMethod("IsNewerThan", BF.NonPublic | BF.Instance) + ?? throw new InvalidOperationException("Could not find VersionStamp.TestAccessor.IsNewerThan method"); + var testAccessor = getTestAccessor.Invoke(version, null) + ?? throw new InvalidOperationException("Could not get test accessor for version"); + var result = (bool?)isNewerThanMethod.Invoke(testAccessor, [other]) + ?? throw new InvalidOperationException("Could not compare versions"); + + return result ? 1 : -1; + } + + // TODO: convert users of this to use SourceText instead + public static ITextSource GetTextSource(this SourceText text) + => new SourceTextTextSource(text); + + // FIXME this is probably inefficient, use it for now but hopefully we can eliminate it + public static string GetText(this SourceText text, int start, int length) + { + var sb = new StringBuilder(length); + for(; start < start + length; start++) + { + sb.Append(text[start]); + } + return sb.ToString(); + } + + class SourceTextTextSource(SourceText text) : ITextSource + { + readonly SourceText text = text; + + public string GetText(int start, int length) + => text.ToString(new TextSpan(start, length)); + + public int Length => text.Length; + + public char this[int index] => text[index]; + + public TextReader CreateReader() + => new SourceTextTextReader(text); + } + + class SourceTextTextReader(SourceText text) : TextReader + { + int position; + + public override int Peek() + { + if(position + 1 < text.Length) + { + return text[position + 1]; + } + return -1; + } + + public override int Read() + { + if(position < text.Length) + { + return text[position++]; + } + return -1; + } + + public override string ReadToEnd() + => text.ToString(new TextSpan(position, text.Length - position)); + } +} diff --git a/MSBuildLanguageServer/Parser/LspXmlParser.cs b/MSBuildLanguageServer/Parser/LspXmlParser.cs new file mode 100644 index 00000000..8f41f22c --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspXmlParser.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.Xml.Analysis; +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Editor.Parsing; +using MonoDevelop.Xml.Parser; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +partial class LspXmlParserService +{ + class LspXmlParser : BackgroundProcessor + { + readonly ILspLogger logger; + readonly LspXmlParserService parserService; + + public LspXmlParser(ILspLogger logger, DocumentId documentId, LspXmlParserService parserService) + : base(NullBackgroundParseService.Instance) + { + DocumentId = documentId; + this.parserService = parserService; + this.logger = logger; + } + + public DocumentId DocumentId { get; } + + protected override Task StartOperationAsync( + EditorDocumentState input, + XmlParseResult? previousOutput, + EditorDocumentState? previousInput, + CancellationToken token) + { + return Task.Run(() => { + var parser = new XmlTreeParser(parserService.StateMachine); + var text = input.Text.Text; + var length = text.Length; + for(int i = 0; i < length; i++) + { + parser.Push(text[i]); + token.ThrowIfCancellationRequested(); + } + var (doc, diagnostics) = parser.EndAllNodes(); + return new XmlParseResult(input, doc, diagnostics, parserService.StateMachine); + }, token); + } + + protected override void OnUnhandledParseError(Exception ex) + { + logger.LogException(ex, "Unhandled XML parser error"); + } + + protected override void OnOperationCompleted(EditorDocumentState input, XmlParseResult output) + { + parserService.OnParseCompleted(output); + } + + public event EventHandler>? ParseCompleted; + + protected override int CompareInputs(EditorDocumentState a, EditorDocumentState b) => a.Text.Version.CompareTo(b.Text.Version); + + public XmlSpineParser GetSpineParser(LinePosition point, SourceText text, CancellationToken token = default) + => LspXmlParserService.GetSpineParser(parserService.StateMachine, LastOutput, point, text, token); + + internal new void StartProcessing(EditorDocumentState document) + { + base.StartProcessing(document); + } + } + + internal static XmlSpineParser GetSpineParser(XmlRootState stateMachine, XmlParseResult? baseline, LinePosition point, SourceText text, CancellationToken token = default) + { + XmlSpineParser? parser = null; + + var offset = point.ToOffset (text); + + if(baseline is not null) + { + var startPos = Math.Min(offset, MaximumCompatiblePosition(baseline.Text, text)); + if(startPos > 0) + { + parser = XmlSpineParser.FromDocumentPosition(stateMachine, baseline.XDocument, startPos); + } + } + + if(parser == null) + { + parser = new XmlSpineParser(stateMachine); + } + + var end = Math.Min(offset, text.Length); + for(int i = parser.Position; i < end; i++) + { + token.ThrowIfCancellationRequested(); + parser.Push(text[i]); + } + + return parser; + + + static int MaximumCompatiblePosition(SourceText oldText, SourceText newText) + { + var changes = newText.GetChangeRanges(oldText); + return changes.Count == 0 ? newText.Length : changes[0].Span.Start; + } + } +} + +class XmlParseResult(EditorDocumentState documentState, XDocument xDocument, IReadOnlyList? diagnostics, XmlRootState stateMachine) +{ + public EditorDocumentState DocumentState => documentState; + public XDocument XDocument => xDocument; + public IReadOnlyList? Diagnostics => diagnostics; + public string FilePath => documentState.FilePath; + public SourceText Text => documentState.Text.Text; + public VersionStamp Version => documentState.Text.Version; + + public XmlSpineParser GetSpineParser(LinePosition point, CancellationToken token = default) + => LspXmlParserService.GetSpineParser(stateMachine, this, point, Text, token); +} diff --git a/MSBuildLanguageServer/Parser/LspXmlParserService.cs b/MSBuildLanguageServer/Parser/LspXmlParserService.cs new file mode 100644 index 00000000..bf62bc34 --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspXmlParserService.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.Xml.Parser; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer; + +partial class LspXmlParserService : ILspService +{ + readonly LspEditorWorkspace workspace; + readonly ILspLogger logger; + + readonly Dictionary parsers = new(); + + public LspXmlParserService(ILspLogger logger, LspEditorWorkspace workspace) + { + this.workspace = workspace; + this.logger = logger; + + // these should be fired serially, guaranteeing serial access to the dictionary + workspace.DocumentOpened += OnDocumentOpened; + workspace.DocumentChanged += OnDocumentChanged; + workspace.DocumentClosed += OnDocumentClosed; + + foreach (var openDoc in workspace.OpenDocuments) + { + OnDocumentOpened(openDoc.CurrentState); + } + } + + public XmlRootState StateMachine { get; } = new(); + + void OnDocumentOpened(object? sender, EditorDocumentEventArgs e) => OnDocumentOpened(e.Document); + + void OnDocumentOpened(EditorDocumentState document) + { + var parser = new LspXmlParser(logger, document.Id, this); + parsers.Add(document.Id, parser); + parser.StartProcessing(document); + } + + void OnDocumentChanged(object? sender, EditorDocumentChangedEventArgs e) + { + var parser = parsers[e.DocumentId]; + parser.StartProcessing(e.Document); + } + + void OnDocumentClosed(object? sender, EditorDocumentEventArgs e) + { + if(parsers.Remove(e.DocumentId, out var parser)) + { + parser.Dispose(); + } + } + + /// Gets a task representing a parse result for the specified document state. It may be completed or running. + /// Returns false if the document was closed + public bool TryGetParseResult(EditorDocumentState document, [NotNullWhen(true)] out Task? task, CancellationToken cancellationToken = default) + { + if (parsers.TryGetValue(document.Id, out var parser)) + { + task = parser.GetOrProcessAsync(document, CancellationToken.None); + return true; + } + + task = null; + return false; + } + + /// Gets the last completed parse result for the specified document. It may be newer than the specified document state. + /// Returns false if the document has not parsed successfully or if the document was closed + public bool TryGetCompletedParseResult(EditorDocumentState document, [NotNullWhen(true)] out XmlParseResult? lastSuccessfulResult) + { + if(parsers.TryGetValue(document.Id, out var parser)) + { + lastSuccessfulResult = parser.LastOutput; + return lastSuccessfulResult is not null; + } + + lastSuccessfulResult = null; + return false; + } + + public XmlSpineParser? GetSpineParser(LinePosition point, EditorDocumentState document, CancellationToken token = default) + { + if(parsers.TryGetValue(document.Id, out var parser)) + { + return parser.GetSpineParser(point, document.Text.Text, token); + } + + return null; + } + + public void SubscribeParseNotification(DocumentId documentId, Action handler) + { + var parser = parsers[documentId]; + parser.ParseCompleted += (e, a) => handler(a.Result); + + if(parser.LastOutput is { } result) + { + handler(result); + } + } + + internal void OnParseCompleted(XmlParseResult result) + { + ParseCompleted?.Invoke(this, new ParseCompletedEventArgs(result.DocumentState.Id, result)); + } + + public event EventHandler>? ParseCompleted; +} diff --git a/MSBuildLanguageServer/Parser/LspXmlParserServiceFactory.cs b/MSBuildLanguageServer/Parser/LspXmlParserServiceFactory.cs new file mode 100644 index 00000000..a2b9aed2 --- /dev/null +++ b/MSBuildLanguageServer/Parser/LspXmlParserServiceFactory.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Parser; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(LspXmlParserService)), Shared] +class LspXmlParserServiceFactory : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + var workspace = lspServices.GetRequiredService(); + return new LspXmlParserService(logger, workspace); + } +} diff --git a/MSBuildLanguageServer/Parser/NullBackgroundParseService.cs b/MSBuildLanguageServer/Parser/NullBackgroundParseService.cs new file mode 100644 index 00000000..27f90651 --- /dev/null +++ b/MSBuildLanguageServer/Parser/NullBackgroundParseService.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.Xml.Editor.Parsing; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Parser; + +class NullBackgroundParseService : IBackgroundParseService +{ + public static NullBackgroundParseService Instance { get; } = new(); + + public bool IsRunning => throw new NotSupportedException(); + + public event EventHandler RunningStateChanged + { + add => throw new NotSupportedException(); + remove => throw new NotSupportedException(); + } + + public void RegisterBackgroundOperation(Task task) + { + } +} diff --git a/MSBuildLanguageServer/Parser/TextPositionConversionExtensions.cs b/MSBuildLanguageServer/Parser/TextPositionConversionExtensions.cs new file mode 100644 index 00000000..5b9f6e6b --- /dev/null +++ b/MSBuildLanguageServer/Parser/TextPositionConversionExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Text; + +using RoslynTextSpan = Microsoft.CodeAnalysis.Text.TextSpan; +using XTextSpan = MonoDevelop.Xml.Dom.TextSpan; + +using LSP = Roslyn.LanguageServer.Protocol; +using MonoDevelop.MSBuild.Language; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Parser; + +static class TextPositionConversionExtensions +{ + public static int ToOffset(this LinePosition position, SourceText text) => text.Lines[position.Line].Start + position.Character; + + public static XTextSpan ToXTextSpan(this LinePositionSpan span, SourceText text) + => XTextSpan.FromBounds(span.Start.ToOffset (text), span.End.ToOffset (text)); + + public static XTextSpan ToRoslynTextSpan(this LinePositionSpan span, SourceText text) + => XTextSpan.FromBounds(span.Start.ToOffset(text), span.End.ToOffset(text)); + + public static LinePositionSpan GetLinePositionSpan(this SourceText sourceText, int start, int length) + => sourceText.Lines.GetLinePositionSpan(new RoslynTextSpan(start, length)); + + public static LinePositionSpan ToLinePositionSpan(this XTextSpan span, SourceText sourceText) + => sourceText.Lines.GetLinePositionSpan(new RoslynTextSpan(span.Start, span.Length)); + + public static LSP.Position GetLspPosition(this SourceText sourceText, int offset) + => ProtocolConversions.LinePositionToPosition(sourceText.Lines.GetLinePosition(offset)); + + public static LSP.Range GetLspRange(this SourceText sourceText, int start, int length) + => ProtocolConversions.TextSpanToRange(new RoslynTextSpan(start, length), sourceText); + + public static LSP.Range ToLspRange(this XTextSpan span, SourceText sourceText) + => ProtocolConversions.TextSpanToRange(span.ToRoslynTextSpan (), sourceText); + + public static LSP.Range ToLspRange(this RoslynTextSpan span, SourceText sourceText) + => ProtocolConversions.TextSpanToRange(span, sourceText); + public static LSP.Range ToLspRange(this MSBuildResolveResult rr, SourceText sourceText) + => new RoslynTextSpan(rr.ReferenceOffset, rr.ReferenceLength).ToLspRange (sourceText); + + public static RoslynTextSpan ToRoslynTextSpan(this XTextSpan span) => new RoslynTextSpan(span.Start, span.Length); + public static XTextSpan ToXTextSpan(this XTextSpan span) => new XTextSpan(span.Start, span.Length); +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Program.cs b/MSBuildLanguageServer/Program.cs new file mode 100644 index 00000000..95b4f191 --- /dev/null +++ b/MSBuildLanguageServer/Program.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +/* +using Nerdbank.Streams; +using StreamJsonRpc; +using Microsoft.VisualStudio.Composition; +using Microsoft.CodeAnalysis.LanguageServer; +using Roslyn.LanguageServer.Protocol; +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; + + +WindowsErrorReporting.SetErrorModeOnWindows(); + +var parser = ProgramHelpers.CreateCommandLineParser(); +return await parser.Parse(args).InvokeAsync(CancellationToken.None); + + + + +var logger = new MSBuildLspLogger (); + +var (clientStream, serverStream) = FullDuplexStream.CreatePair(); + +var messageFormatter = new JsonMessageFormatter(); +var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverStream, serverStream, messageFormatter)); + +// TODO: return compositionConfiguration.CompositionErrors over JsonRpc +var catalog = await MSBuildLspCatalog.Create (); + +ICapabilitiesProvider capabilitiesProvider = catalog.ExportProvider.GetExportedValue (); +ILanguageServerFactory languageServerFactory = catalog.ExportProvider.GetExportedValue (); + +var host = new LanguageServerHost (serverStream, serverStream, catalog.ExportProvider, logger); + +host.Start(); + +var server = languageServerFactory.Create ( + jsonRpc, + capabilitiesProvider, + WellKnownLspServerKinds.MSBuild, + logger, + new Microsoft.CodeAnalysis.Host.HostServices () +); + +jsonRpc.StartListening(); +server.Initialize(); + +[Export(typeof(MSBuildCapabilitiesProvider)), Shared] +class MSBuildCapabilitiesProvider : ICapabilitiesProvider +{ + public ServerCapabilities GetCapabilities (ClientCapabilities clientCapabilities) + { + throw new NotImplementedException (); + } +} + +/* +class MSBuildLanguageServer : AbstractLanguageServer, IOnInitialized +{ + readonly ImmutableDictionary>> _baseServices; + + public MSBuildLanguageServer (JsonRpc jsonRpc, ILspLogger logger) : base (jsonRpc, logger) + { + Initialize(); + } + + public Task OnInitializedAsync (ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException (); + } + + protected override ILspServices ConstructLspServices () + { + throw new NotImplementedException (); + } +}*/ +/* +[Export (typeof (IMethodHandler))] +class InitializeHandler : IInitializeManager +{ + public InitializeParams GetInitializeParams () + { + throw new NotImplementedException (); + } + + public InitializeResult GetInitializeResult () + { + throw new NotImplementedException (); + } + + public void SetInitializeParams (InitializeParams request) + { + throw new NotImplementedException (); + } +} + +[Export (typeof (IMethodHandler))] +class TextDocumentDidChangeHandler : ILspDocumentRequestHandler +{ + public bool MutatesSolutionState => throw new NotImplementedException (); + + public TextDocumentIdentifier GetTextDocumentIdentifier (DidChangeTextDocumentParams request) => request.TextDocument; + + [LanguageServerEndpoint(Methods.TextDocumentDidChangeName)] + public Task HandleRequestAsync (DidChangeTextDocumentParams request, MSBuildRequestContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException (); + } +} + +[Export (typeof (IMethodHandler))] +class DocClosedHandler : ILspNotificationHandler +{ + public bool MutatesSolutionState => throw new NotImplementedException (); + + [LanguageServerEndpoint(Methods.TextDocumentDidChangeName)] + public Task HandleNotificationAsync (DidCloseTextDocumentParams request, MSBuildRequestContext requestContext, CancellationToken cancellationToken) + { + throw new NotImplementedException (); + } +} +*/ \ No newline at end of file diff --git a/MSBuildLanguageServer/Services/CompletionListCacheFactory.cs b/MSBuildLanguageServer/Services/CompletionListCacheFactory.cs new file mode 100644 index 00000000..44e2fa6b --- /dev/null +++ b/MSBuildLanguageServer/Services/CompletionListCacheFactory.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler.Completion; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(CompletionListCache)), Shared] +internal class CompletionListCacheFactory : ILspServiceFactory +{ + [ImportingConstructor] + public CompletionListCacheFactory() + { + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) => new CompletionListCache(); +} + +class CompletionListCache : ResolveCache +{ + public CompletionListCache() : base(maxCacheSize: 3) { } +} + +record CompletionListCacheEntry(List Items, CompletionRenderContext Context) { } \ No newline at end of file diff --git a/MSBuildLanguageServer/Services/FunctionTypeProviderServiceFactory.cs b/MSBuildLanguageServer/Services/FunctionTypeProviderServiceFactory.cs new file mode 100644 index 00000000..c9691421 --- /dev/null +++ b/MSBuildLanguageServer/Services/FunctionTypeProviderServiceFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer; +using MonoDevelop.MSBuild.Editor.Roslyn; +using MonoDevelop.MSBuild.Language; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(FunctionTypeProviderService)), Shared] +class FunctionTypeProviderServiceFactory : ILspServiceFactory +{ + readonly IRoslynCompilationProvider compilationProvider; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public FunctionTypeProviderServiceFactory(IRoslynCompilationProvider compilationProvider) + { + this.compilationProvider = compilationProvider; + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + return new FunctionTypeProviderService(compilationProvider, logger); + } +} + +class FunctionTypeProviderService : ILspService +{ + public IFunctionTypeProvider FunctionTypeProvider { get; } + + public FunctionTypeProviderService(IRoslynCompilationProvider compilationProvider, ILspLogger logger) + { + var log = logger.ToILogger(); + this.FunctionTypeProvider = new RoslynFunctionTypeProvider(compilationProvider, log); + } +} + +[Export(typeof(IRoslynCompilationProvider))] +class SimpleRoslynCompilationProvider : IRoslynCompilationProvider +{ + public MetadataReference CreateReference(string assemblyPath) + => MetadataReference.CreateFromFile(assemblyPath); +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Services/LspNavigationService.FindReferencesSearchJob.cs b/MSBuildLanguageServer/Services/LspNavigationService.FindReferencesSearchJob.cs new file mode 100644 index 00000000..d46a9766 --- /dev/null +++ b/MSBuildLanguageServer/Services/LspNavigationService.FindReferencesSearchJob.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.Text; + +using MonoDevelop.Xml.Dom; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +partial class LspNavigationService +{ + class FindReferencesSearchJob + { + public FindReferencesSearchJob(string filename, XDocument? document, SourceText? sourceText) + { + Filename = filename; + Document = document; + SourceText = sourceText; + } + + public string Filename { get; } + public XDocument? Document { get; set; } + public SourceText? SourceText { get; set; } + } +} diff --git a/MSBuildLanguageServer/Services/LspNavigationService.cs b/MSBuildLanguageServer/Services/LspNavigationService.cs new file mode 100644 index 00000000..af41e758 --- /dev/null +++ b/MSBuildLanguageServer/Services/LspNavigationService.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Handler; +using MonoDevelop.MSBuild.Editor.LanguageServer.Parser; +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; +using MonoDevelop.MSBuild.Editor.Navigation; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Parser; + +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +using LSP = Roslyn.LanguageServer.Protocol; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +partial class LspNavigationService(LspEditorWorkspace workspace, LspXmlParserService xmlParserService, ILogger logger) : ILspService +{ + public Task FindReferences( + MSBuildParseResult originParseResult, + MSBuildReferenceCollectorFactory collectorFactory, + BufferedProgress resultReporter, + IProgress? progress, + CancellationToken cancellationToken, + Func? resultFilter = null) + { + return FindReferencesInternal( + originParseResult, + ReferencesToLocations, + collectorFactory, + resultReporter, + progress, + cancellationToken, + resultFilter + ); + } + + public Task FindReferences( + MSBuildParseResult originParseResult, + LSP.Range originRange, + MSBuildReferenceCollectorFactory collectorFactory, + BufferedProgress resultReporter, + IProgress? progress, + CancellationToken cancellationToken, + Func? resultFilter = null) + { + return FindReferencesInternal( + originParseResult, + (filename, sourceText, references) => ReferencesToLocationLinks(filename, sourceText, originRange, references), + collectorFactory, + resultReporter, + progress, + cancellationToken, + resultFilter + ); + } + + + async Task FindReferencesInternal( + MSBuildParseResult originParseResult, + Func, TResult[]> createResults, + MSBuildReferenceCollectorFactory collectorFactory, + BufferedProgress resultReporter, + IProgress? progress, + CancellationToken cancellationToken, + Func? resultFilter = null) + { + var openDocuments = workspace.OpenDocuments.ToDictionary(d => d.FilePath, PathUtilities.Comparer); + + var originFilename = originParseResult.MSBuildDocument.Filename!; + + var jobs = originParseResult.MSBuildDocument.GetDescendentImports() + .Where(imp => imp.IsResolved) + .Select(imp => new FindReferencesSearchJob(imp.Filename!, null, null)) + .Prepend( + new FindReferencesSearchJob( + originFilename, + originParseResult.XmlParseResult.XDocument, + originParseResult.XmlParseResult.Text)) + .ToList(); + + int jobsCompleted = 0; + object reportLock = new (); + int percentageReported = 0; + + await Parallel.ForEachAsync(jobs, cancellationToken, async (job, token) => { + try + { + var locations = await ProcessSearchJob( + originParseResult.MSBuildDocument, + createResults, + job, + openDocuments, + collectorFactory, + xmlParserService, + logger, + token + ).ConfigureAwait(false); + + if(locations is not null) + { + resultReporter.Report(locations); + } + + if(progress is not null) + { + // increment the job completion count. + // if the increment causes the percentage to increase, then report it. + int updatedJobsCompleted = Interlocked.Increment(ref jobsCompleted); + int oldPercentage = (int)Math.Floor((double)(updatedJobsCompleted - 1) / jobs.Count); + int newPercentage = (int)Math.Floor((double)updatedJobsCompleted / jobs.Count); + if(newPercentage > oldPercentage) + { + lock(reportLock) + { + if (percentageReported < newPercentage) + { + percentageReported = newPercentage; + progress.Report(percentage: newPercentage); + } + } + } + } + + } catch(Exception ex) + { + MSBuildNavigationHelpers.LogErrorSearchingFile(logger, ex, job.Filename); + } + }); + } + + static async Task ProcessSearchJob( + MSBuildRootDocument originDocument, + Func, TResult[]> createResults, + FindReferencesSearchJob job, + Dictionary openDocuments, + MSBuildReferenceCollectorFactory collectorFactory, + LspXmlParserService xmlParserService, + ILogger logger, + CancellationToken cancellationToken) + { + var filename = job.Filename; + var sourceText = job.SourceText; + var document = job.Document; + + if(sourceText is null) + { + if(openDocuments.TryGetValue(filename, out var openDocument)) + { + sourceText = openDocument.CurrentState.Text.Text; + if(xmlParserService.TryGetParseResult(openDocument.CurrentState, out var parseTask, cancellationToken)) + { + document = (await parseTask.ConfigureAwait(false)).XDocument; + } + } else + { + if(!File.Exists(filename)) + { + // TODO: log this? + return null; + } + using var file = File.OpenRead(filename); + sourceText = SourceText.From(file); + } + } + + if(document is null) + { + var xmlParser = new XmlTreeParser(new XmlRootState()); + (document, _) = ParseSourceText(sourceText, xmlParserService.StateMachine, cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if(document.RootElement is null) + { + return null; + } + + var references = new List(); + + // the collector only uses the MSBuildDocument to resolve schemas, + // so we can use the root document here. + var collector = collectorFactory(originDocument, sourceText.GetTextSource(), logger, references.Add); + collector.Run(document.RootElement, token: cancellationToken); + + if (references.Count == 0) + { + return null; + } + + return createResults(filename, sourceText, references); + } + + static (XDocument document, IReadOnlyList? diagnostics) ParseSourceText(SourceText text, XmlRootState stateMachine, CancellationToken cancellationToken) + { + var parser = new XmlTreeParser(stateMachine); + var length = text.Length; + for(int i = 0; i < length; i++) + { + parser.Push(text[i]); + cancellationToken.ThrowIfCancellationRequested(); + } + return parser.EndAllNodes(); + } + + static LocationLink[] ReferencesToLocationLinks(string targetFilePath, SourceText targetSourceText, LSP.Range originRange, List references) + { + var targetUri = ProtocolConversions.CreateAbsoluteUri(targetFilePath); + + var locations = references.Select(reference => { + var range = targetSourceText.GetLspRange(reference.Offset, reference.Length); + return new LocationLink { + OriginSelectionRange = originRange, + TargetUri = targetUri, + TargetRange = range, + TargetSelectionRange = range + }; + }).ToArray(); + + return locations; + } + + static Location[] ReferencesToLocations(string targetFilePath, SourceText targetSourceText, List references) + { + var targetUri = ProtocolConversions.CreateAbsoluteUri(targetFilePath); + + var locations = references.Select(reference => { + var range = targetSourceText.GetLspRange(reference.Offset, reference.Length); + return new Location { + Uri = targetUri, + Range = range + }; + }).ToArray(); + + return locations; + } +} diff --git a/MSBuildLanguageServer/Services/LspNavigationServiceFactory.cs b/MSBuildLanguageServer/Services/LspNavigationServiceFactory.cs new file mode 100644 index 00000000..26b80d7c --- /dev/null +++ b/MSBuildLanguageServer/Services/LspNavigationServiceFactory.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +using MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(LspNavigationService)), Shared] +internal class LspNavigationServiceFactory : ILspServiceFactory +{ + [ImportingConstructor] + public LspNavigationServiceFactory() + { + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + var extLogger = logger.ToILogger(); + var workspaceService = lspServices.GetRequiredService(); + var xmlParserService = lspServices.GetRequiredService(); + return new LspNavigationService(workspaceService, xmlParserService, extLogger); + } +} diff --git a/MSBuildLanguageServer/Services/MSBuildRuntimeServiceFactory.cs b/MSBuildLanguageServer/Services/MSBuildRuntimeServiceFactory.cs new file mode 100644 index 00000000..8618d472 --- /dev/null +++ b/MSBuildLanguageServer/Services/MSBuildRuntimeServiceFactory.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; + +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Services; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(MSBuildRuntimeService)), Shared] +class MSBuildRuntimeServiceFactory : ILspServiceFactory +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public MSBuildRuntimeServiceFactory() + { + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var logger = lspServices.GetRequiredService(); + return new MSBuildRuntimeService(logger); + } +} + +// TODO: this is the beginning of an abstraction that will eventually allow us to load different MSBuild versions +class MSBuildRuntimeService : ILspService +{ + static object gate = new(); + static bool initialized = false; + + public MSBuildRuntimeService(ILspLogger logger) + { + if(!initialized) + { + lock(gate) + { + if(!initialized) + { + initialized = true; + Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + } + } + } + + var log = logger.ToILogger(); + try + { + MSBuildEnvironment = new CurrentProcessMSBuildEnvironment(log); + } catch(Exception ex) + { + logger.LogException(ex, "Failed to initialize MSBuild runtime info"); + MSBuildEnvironment = new NullMSBuildEnvironment(); + } + } + + public IMSBuildEnvironment MSBuildEnvironment { get; } +} diff --git a/MSBuildLanguageServer/Workspace/EditorDocumentState.cs b/MSBuildLanguageServer/Workspace/EditorDocumentState.cs new file mode 100644 index 00000000..208b1435 --- /dev/null +++ b/MSBuildLanguageServer/Workspace/EditorDocumentState.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +class EditorDocumentState (DocumentId id, string filePath, TextAndVersion text) +{ + public DocumentId Id => id; + public string FilePath => filePath; + public TextAndVersion Text => text ?? throw new InvalidOperationException("Text not yet loaded"); +} diff --git a/MSBuildLanguageServer/Workspace/LspEditorDocument.cs b/MSBuildLanguageServer/Workspace/LspEditorDocument.cs new file mode 100644 index 00000000..5563e9da --- /dev/null +++ b/MSBuildLanguageServer/Workspace/LspEditorDocument.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +class LspEditorDocument +{ + EditorDocumentState state; + + public EditorDocumentState CurrentState => state; + + public DocumentId Id => state.Id; + public string FilePath => state.FilePath; + public TextAndVersion Text => state.Text; + + public LspEditorDocument(DocumentId id, string filePath, SourceText initialText) + { + state = new EditorDocumentState(id, filePath, TextAndVersion.Create(initialText, VersionStamp.Default)); + } + + internal void UpdateText (SourceText text) + { + EditorDocumentState oldState = state; + state = new EditorDocumentState ( + oldState.Id, + oldState.FilePath, + TextAndVersion.Create(text, oldState.Text?.Version.GetNewerVersion() ?? VersionStamp.Default) + ); + } +} diff --git a/MSBuildLanguageServer/Workspace/LspEditorWorkspace.cs b/MSBuildLanguageServer/Workspace/LspEditorWorkspace.cs new file mode 100644 index 00000000..6d8b7abd --- /dev/null +++ b/MSBuildLanguageServer/Workspace/LspEditorWorkspace.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Text; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +class LspEditorWorkspace : ILspService, IDocumentChangeTracker +{ + // RequestExecutionQueue guarantees serial access to this + readonly Dictionary documents = new(); + ImmutableDictionary _trackedDocuments + = ImmutableDictionary.Empty; + + public LspEditorDocument GetEditorDocument(Uri documentUri) + { + if (documents.TryGetValue(documentUri, out var doc)) { + return doc; + } + throw new InvalidOperationException("Document not open"); + } + + public ValueTask StartTrackingAsync(Uri documentUri, SourceText initialText, string languageId, CancellationToken cancellationToken) + { + if(documents.TryGetValue(documentUri, out var document)) + { + throw new InvalidOperationException("Already tracking document"); + + } + + var documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()); + document = new LspEditorDocument (documentId, ProtocolConversions.GetDocumentFilePathFromUri (documentUri), initialText); + documents.Add(documentUri, document); + _trackedDocuments = _trackedDocuments.Add(documentUri, (initialText, languageId)); + + DocumentOpened?.Invoke(this, new EditorDocumentEventArgs(document.CurrentState)); + + return ValueTask.CompletedTask; + } + + public void UpdateTrackedDocument(Uri documentUri, SourceText text) + { + var doc = GetEditorDocument(documentUri); + var oldState = doc.CurrentState; + doc.UpdateText(text); + var newState = doc.CurrentState; + + _trackedDocuments = _trackedDocuments.SetItem(documentUri, (text, _trackedDocuments[documentUri].LanguageId)); + + DocumentChanged?.Invoke(this, new EditorDocumentChangedEventArgs(newState, oldState)); + } + + public ValueTask StopTrackingAsync(Uri documentUri, CancellationToken cancellationToken) + { + _trackedDocuments = _trackedDocuments.Remove(documentUri); + + if (!documents.Remove(documentUri, out var document)) + { + throw new InvalidOperationException("Document not open"); + } + + DocumentClosed?.Invoke(this, new EditorDocumentEventArgs(document.CurrentState)); + + return ValueTask.CompletedTask; + } + + public ImmutableDictionary GetTrackedLspText() => _trackedDocuments; + + public IReadOnlyCollection OpenDocuments => documents.Values.ToArray(); + + public event EventHandler? DocumentOpened; + public event EventHandler? DocumentChanged; + public event EventHandler? DocumentClosed; +} + +class EditorDocumentChangedEventArgs(EditorDocumentState newDocument, EditorDocumentState oldDocument) + : EditorDocumentEventArgs(newDocument) +{ + public EditorDocumentState OldState { get; } = oldDocument; +} + +class EditorDocumentEventArgs(EditorDocumentState document) : EventArgs +{ + public EditorDocumentState Document { get; } = document; + public DocumentId DocumentId { get; } = document.Id; +} \ No newline at end of file diff --git a/MSBuildLanguageServer/Workspace/LspEditorWorkspaceFactory.cs b/MSBuildLanguageServer/Workspace/LspEditorWorkspaceFactory.cs new file mode 100644 index 00000000..58d72097 --- /dev/null +++ b/MSBuildLanguageServer/Workspace/LspEditorWorkspaceFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Composition; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; + +namespace MonoDevelop.MSBuild.Editor.LanguageServer.Workspace; + +[ExportCSharpVisualBasicLspServiceFactory(typeof(LspEditorWorkspace)), Shared] +class LspEditorWorkspaceFactory : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + return new LspEditorWorkspace(); + } +} diff --git a/MonoDevelop.MSBuild.Editor.Common/Completion/CompletionHelpers.cs b/MonoDevelop.MSBuild.Editor.Common/Completion/CompletionHelpers.cs new file mode 100644 index 00000000..1142f4a3 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/Completion/CompletionHelpers.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.MSBuild.Language.Syntax; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.Xml.Dom; + +namespace MonoDevelop.MSBuild.Editor.Completion; + +// code extracted from MSBuildCompletionSource for use by LanguageServer +internal class CompletionHelpers +{ + //FIXME: improve logic for determining where metadata is permitted + public static bool IsMetadataAllowed (ExpressionNode triggerExpression, MSBuildResolveResult rr) + { + //if any a parent node is an item transform or function, metadata is allowed + if (triggerExpression != null) { + var node = triggerExpression.Find (triggerExpression.Length); + while (node != null) { + if (node is ExpressionItemTransform || node is ExpressionItemFunctionInvocation) { + return true; + } + node = node.Parent; + } + } + + if (rr.AttributeSyntax != null) { + switch (rr.AttributeSyntax.SyntaxKind) { + // metadata attributes on items can refer to other metadata on the items + case MSBuildSyntaxKind.Item_Metadata: + // task params can refer to metadata in batched items + case MSBuildSyntaxKind.Task_Parameter: + // target inputs and outputs can use metadata from each other's items + case MSBuildSyntaxKind.Target_Inputs: + case MSBuildSyntaxKind.Target_Outputs: + return true; + //conditions on metadata elements can refer to metadata on the items + case MSBuildSyntaxKind.Metadata_Condition: + return true; + } + } + + if (rr.ElementSyntax != null) { + switch (rr.ElementSyntax.SyntaxKind) { + // metadata elements can refer to other metadata in the items + case MSBuildSyntaxKind.Metadata: + return true; + } + } + return false; + } + + public static bool ShouldAddHintForCompletions (ITypedSymbol symbol) + => symbol.ValueKindWithoutModifiers () switch { + MSBuildValueKind.WarningCode => true, + MSBuildValueKind.CustomType when symbol.CustomType is CustomTypeInfo ct => ct.BaseKind switch { + MSBuildValueKind.Guid => true, + MSBuildValueKind.Int => true, + MSBuildValueKind.WarningCode => true, + _ => false + }, + _ => false + }; + + public static bool TryGetElementSyntaxForElementCompletion (List nodePath, out MSBuildElementSyntax? languageElement, out string? elementName) + { + // we can't use the LanguageElement from the resolveresult for element completion. + // if completion is triggered in an existing element's name, the resolveresult + // will be for that element, so completion will be for the element's children + // rather than for the element itself. + languageElement = null; + elementName = null; + for (int i = 1; i < nodePath.Count; i++) { + if (nodePath[i] is XElement el) { + elementName = el.Name.Name; + languageElement = MSBuildElementSyntax.Get (elementName, languageElement); + continue; + } + return false; + } + + // if we don't have a language element and we're not at root level, we're in an invalid location + if (languageElement == null && nodePath.Count > 2) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.Common/Completion/MSBuildCompletionTrigger.cs b/MonoDevelop.MSBuild.Editor.Common/Completion/MSBuildCompletionTrigger.cs new file mode 100644 index 00000000..b93b1ab2 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/Completion/MSBuildCompletionTrigger.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.MSBuild.Language.Syntax; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.Xml.Parser; + +namespace MonoDevelop.MSBuild.Editor.Completion; + +internal record class MSBuildCompletionTrigger( + MSBuildResolveResult ResolveResult, ExpressionCompletion.TriggerState TriggerState, + int SpanStart, int SpanLength, + ExpressionNode Expression, string ExpressionText, + ExpressionCompletion.ListKind ListKind, IReadOnlyList ComparandVariables) +{ + public static MSBuildCompletionTrigger? TryCreate( + XmlSpineParser spine, ITextSource textSource, ExpressionCompletion.ExpressionTriggerReason reason, + int offset, char typedCharacter, Microsoft.Extensions.Logging.ILogger logger, + IFunctionTypeProvider functionTypeProvider, MSBuildResolveResult? resolveResult, CancellationToken cancellationToken) + { + if(!ExpressionCompletion.IsPossibleExpressionCompletionContext(spine)) + { + return null; + } + + // the resolver may modify the spine, so clone it + // NOTE: this resolver uses an empty root document, so it won't resolve symbols correctly. + // that's okay, as long as we don't try to use them, and only use the resolved syntax. + var rr = resolveResult ?? MSBuildResolver.Resolve(spine.Clone(), textSource, MSBuildRootDocument.Empty, functionTypeProvider, logger, cancellationToken); + + if(rr?.ElementSyntax is MSBuildElementSyntax elementSyntax && (rr.Attribute is not null || elementSyntax.ValueKind != MSBuildValueKind.Nothing)) + { + // TryGetIncompleteValue may return false while still outputting incomplete values, if it fails due to reaching maximum readahead. + // It will also return false and output null values if we're in an element value that only contains whitespace. + // In both these cases we can ignore the false return and proceed anyways. + spine.TryGetIncompleteValue (textSource, out var expressionText, out var valueSpan, cancellationToken: cancellationToken); + expressionText ??= ""; + int exprStartPos = valueSpan?.Start ?? offset; + + var triggerState = ExpressionCompletion.GetTriggerState( + expressionText, + offset - exprStartPos, + reason, + typedCharacter, + rr.IsCondition(), + out int spanStart, + out int spanLength, + out ExpressionNode expression, + out ExpressionCompletion.ListKind listKind, + out IReadOnlyList comparandVariables, + logger + ); + + if(triggerState != ExpressionCompletion.TriggerState.None) + { + return new(rr, triggerState, exprStartPos + spanStart, spanLength, expression, expressionText, listKind, comparandVariables); + } + } + return null; + } +} diff --git a/MonoDevelop.MSBuild.Editor.Common/Completion/PackageCompletion.cs b/MonoDevelop.MSBuild.Editor.Common/Completion/PackageCompletion.cs new file mode 100644 index 00000000..fb7ef854 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/Completion/PackageCompletion.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.MSBuild.Language.Syntax; +using MonoDevelop.MSBuild.Language.Typesystem; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.Xml.Dom; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace MonoDevelop.MSBuild.Editor.Completion; + +static class PackageCompletion +{ + public static bool TryGetPackageVersionSearchJob ( + MSBuildResolveResult? rr, + MSBuildRootDocument doc, + IPackageSearchManager packageSearchManager, + [NotNullWhen(true)] out IPackageFeedSearchJob>? packageFeedSearchJob, + [NotNullWhen (true)] out string? packageId, + out string? targetFrameworkSearchParameter + ) + { + targetFrameworkSearchParameter = null; + packageId = null; + packageFeedSearchJob = null; + + if (rr is null || GetItemGroupItemFromMetadata (rr) is not XElement itemEl || GetIncludeOrUpdateAttribute (itemEl) is not XAttribute includeAtt) { + return false; + } + + // we can only provide version completions if the item's value type is non-list nugetid + var itemInfo = doc.GetSchemas ().GetItem (itemEl.Name.Name); + if (itemInfo == null || !itemInfo.ValueKind.IsKindOrListOfKind (MSBuildValueKind.NuGetID)) { + return false; + } + + var packageType = itemInfo.CustomType?.Values[0].Name; + + packageId = includeAtt.Value; + if (string.IsNullOrEmpty (packageId)) { + return false; + } + + // check it's a non-list literal value, we can't handle anything else + var expr = ExpressionParser.Parse (packageId, ExpressionOptions.ItemsMetadataAndLists); + if (expr.NodeKind != ExpressionNodeKind.Text) { + return false; + } + + targetFrameworkSearchParameter = doc.GetTargetFrameworkNuGetSearchParameter (); + + packageFeedSearchJob = packageSearchManager.SearchPackageVersions (packageId.ToLower (), targetFrameworkSearchParameter, packageType); + return true; + } + + static bool ItemIsInItemGroup (XElement itemEl) => itemEl.Parent is XElement parent && parent.Name.Equals (MSBuildElementSyntax.ItemGroup.Name, true); + + static XElement? GetItemGroupItemFromMetadata (MSBuildResolveResult rr) + => rr.ElementSyntax.SyntaxKind switch { + MSBuildSyntaxKind.Item => rr.Element, + MSBuildSyntaxKind.Metadata => rr.Element.Parent is XElement parentEl && ItemIsInItemGroup (parentEl) ? parentEl : null, + _ => null + }; + + static XAttribute? GetIncludeOrUpdateAttribute (XElement item) + => item.Attributes.FirstOrDefault (att => MSBuildElementSyntax.Item.GetAttribute (att)?.SyntaxKind switch { + MSBuildSyntaxKind.Item_Include => true, + MSBuildSyntaxKind.Item_Update => true, + _ => false + }); +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.Common/Completion/SdkCompletion.cs b/MonoDevelop.MSBuild.Editor.Common/Completion/SdkCompletion.cs new file mode 100644 index 00000000..c0378953 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/Completion/SdkCompletion.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.IO; +using System.Threading; + +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.SdkResolution; + +namespace MonoDevelop.MSBuild.Editor.Completion; + +internal static class SdkCompletion +{ + //FIXME: SDK version completion + //FIXME: enumerate SDKs from NuGet + public static List GetSdkCompletions (MSBuildRootDocument doc, ILogger logger, CancellationToken token) + { + var items = new List (); + var sdks = new HashSet (); + + foreach (var sdk in doc.Environment.GetRegisteredSdks ()) { + if (sdks.Add (sdk.Name)) { + items.Add (sdk); + } + } + + //FIXME we should be able to cache these + doc.Environment.ToolsetProperties.TryGetValue (WellKnownProperties.MSBuildSDKsPath, out var sdksPath); + if (sdksPath != null) { + AddSdksFromDir (sdksPath); + } + + var dotNetSdk = doc.Environment.ResolveSdk (new ("Microsoft.NET.Sdk", null, null), null, null, logger); + if (dotNetSdk?.Path is string sdkPath) { + string? dotNetSdkPath = Path.GetDirectoryName (Path.GetDirectoryName (sdkPath)); + if (dotNetSdkPath is not null && (sdksPath is null || Path.GetFullPath (dotNetSdkPath) != Path.GetFullPath (sdksPath))) { + AddSdksFromDir (dotNetSdkPath); + } + } + + void AddSdksFromDir (string sdkDir) + { + if (!Directory.Exists (sdkDir)) { + return; + } + foreach (var dir in Directory.GetDirectories (sdkDir)) { + string name = Path.GetFileName (dir); + var targetsFileExists = File.Exists (Path.Combine (dir, "Sdk", "Sdk.targets")); + if (targetsFileExists && sdks.Add (name)) { + items.Add (new SdkInfo (name, null, Path.Combine (dir, name))); + } + } + } + + return items; + } +} diff --git a/MonoDevelop.MSBuild.Editor.Common/InternalsVisibleTo.cs b/MonoDevelop.MSBuild.Editor.Common/InternalsVisibleTo.cs new file mode 100644 index 00000000..f53feb22 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/InternalsVisibleTo.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; +using MonoDevelop.MSBuild; + +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Tests, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Tests.Editor, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MSBuildLanguageServer.Tests, {IVT.PublicKeyAtt}")] + +[assembly: InternalsVisibleTo ($"MSBuildLanguageServer, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Editor, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Editor.VisualStudio, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuildEditor, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"Microsoft.Ide.LanguageService.MSBuild, PublicKey=0024000004800000940000000602000000240000525341310004000001000100675da410943cdcf89a2bbd3716e451b3c35c0de9278a874e06d143dbc861f7b4d21771131177e413290078b98615421b2bb9ac25c14021c4e2c7b967407b5ea96417317ff8bdb1ef34e0d63f5965bdf92841bdaae505987af712a2e1951b2ff76a16d211e0d5ae2c444f55dbd0a3c0f5bed051af0cf7bae49114c4e0c527c4ed")] diff --git a/MonoDevelop.MSBuild.Editor.Common/MonoDevelop.MSBuild.Editor.Common.csproj b/MonoDevelop.MSBuild.Editor.Common/MonoDevelop.MSBuild.Editor.Common.csproj new file mode 100644 index 00000000..9c6da44d --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/MonoDevelop.MSBuild.Editor.Common.csproj @@ -0,0 +1,77 @@ + + + + net48;net8.0 + True + True + $(NoWarn);1591;1573 + enable + MonoDevelop.MSBuild.Editor + + $(DefineConstants);CODE_STYLE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.Common/Navigation/MSBuildNavigationHelpers.cs b/MonoDevelop.MSBuild.Editor.Common/Navigation/MSBuildNavigationHelpers.cs new file mode 100644 index 00000000..da4021b4 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor.Common/Navigation/MSBuildNavigationHelpers.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable annotations + +using System; + +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.Xml.Logging; +using MonoDevelop.Xml.Parser; + +namespace MonoDevelop.MSBuild.Editor.Navigation; + +static partial class MSBuildNavigationHelpers +{ + public static string GetFindReferencesSearchTitle (MSBuildResolveResult reference, ILogger logger) + { + string referenceName = reference.GetReferenceDisplayName (); + + return reference.ReferenceKind switch { + MSBuildReferenceKind.Item => $"Item '{referenceName}' references", + MSBuildReferenceKind.Property => $"Property '{referenceName}' references", + MSBuildReferenceKind.Metadata => $"Metadata '{referenceName}' references", + MSBuildReferenceKind.Task => $"Task '{referenceName}' references", + MSBuildReferenceKind.TaskParameter => $"Task parameter '{referenceName}' references", + MSBuildReferenceKind.Keyword => $"Keyword '{referenceName}' references", + MSBuildReferenceKind.Target => $"Target '{referenceName}' references", + MSBuildReferenceKind.KnownValue => $"Value '{referenceName}' references", + MSBuildReferenceKind.NuGetID => $"NuGet package '{referenceName}' references", + MSBuildReferenceKind.TargetFramework => $"Target framework '{referenceName}' references", + MSBuildReferenceKind.ItemFunction => $"Item function '{referenceName}' references", + MSBuildReferenceKind.PropertyFunction => $"Property function '{referenceName}' references", + MSBuildReferenceKind.StaticPropertyFunction => $"Static '{referenceName}' references", + MSBuildReferenceKind.ClassName => $"Class '{referenceName}' references", + MSBuildReferenceKind.Enum => $"Enum '{referenceName}' references", + MSBuildReferenceKind.ConditionFunction => $"Condition function '{referenceName}' references", + MSBuildReferenceKind.FileOrFolder => $"Path '{referenceName}' references", + MSBuildReferenceKind.TargetFrameworkIdentifier => $"TargetFrameworkIdentifier '{referenceName}' references", + MSBuildReferenceKind.TargetFrameworkVersion => $"TargetFrameworkVersion '{referenceName}' references", + MSBuildReferenceKind.TargetFrameworkProfile => $"TargetFrameworkProfile '{referenceName}' references", + _ => logger.LogUnhandledCaseAndReturnDefaultValue ($"'{referenceName}' references", reference.ReferenceKind) + }; + } + + public static string GetFindTargetDefinitionsSearchTitle (string targetName) => $"Target '{targetName}' definitions"; + + public static string GetFindPropertyWritesSearchTitle (string propertyName) => $"Property '{propertyName}' writes"; + + public static string GetFindItemWritesSearchTitle (string itemName) => $"Item '{itemName}' writes"; + + public static bool FilterUsageWrites (FindReferencesResult result) => result.Usage switch { + ReferenceUsage.Declaration or ReferenceUsage.Write => true, + _ => false + }; + + [LoggerMessage (EventId = 0, Level = LogLevel.Warning, Message = "Error searching for references in MSBuild file '{filename}'")] + public static partial void LogErrorSearchingFile (ILogger logger, Exception ex, UserIdentifiableFileName filename); + + [LoggerMessage (EventId = 1, Level = LogLevel.Error, Message = "Error getting text for file '{filename}'")] + public static partial void LogErrorGettingFileText (ILogger logger, Exception ex, UserIdentifiableFileName filename); +} + +delegate MSBuildReferenceCollector MSBuildReferenceCollectorFactory (MSBuildDocument doc, ITextSource textSource, ILogger logger, FindReferencesReporter reportResult); \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/Roslyn/IRoslynCompilationProvider.cs b/MonoDevelop.MSBuild.Editor.Common/Roslyn/IRoslynCompilationProvider.cs similarity index 71% rename from MonoDevelop.MSBuild.Editor/Roslyn/IRoslynCompilationProvider.cs rename to MonoDevelop.MSBuild.Editor.Common/Roslyn/IRoslynCompilationProvider.cs index af41d616..f1f5a4bf 100644 --- a/MonoDevelop.MSBuild.Editor/Roslyn/IRoslynCompilationProvider.cs +++ b/MonoDevelop.MSBuild.Editor.Common/Roslyn/IRoslynCompilationProvider.cs @@ -5,6 +5,10 @@ namespace MonoDevelop.MSBuild.Editor.Roslyn { + /// + /// Allows consumers of to control how it loads + /// assemblies. + /// public interface IRoslynCompilationProvider { MetadataReference CreateReference (string assemblyPath); diff --git a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionInfo.cs b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionInfo.cs similarity index 97% rename from MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionInfo.cs rename to MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionInfo.cs index fb4b4af8..58c92a75 100644 --- a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionInfo.cs +++ b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionInfo.cs @@ -71,7 +71,7 @@ public RoslynPropertyInfo (IPropertySymbol symbol, MSBuildValueKind? arrayElemen public IPropertySymbol Symbol { get; } public override MSBuildValueKind ReturnType => arrayElementType ?? RoslynFunctionTypeProvider.ConvertType (Symbol.Type); - public override FunctionParameterInfo [] Parameters => null; + public override FunctionParameterInfo [] Parameters => []; public override bool IsProperty => true; } } \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionTypeProvider.cs b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionTypeProvider.cs similarity index 95% rename from MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionTypeProvider.cs rename to MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionTypeProvider.cs index 363d2065..a04af511 100644 --- a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynFunctionTypeProvider.cs +++ b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynFunctionTypeProvider.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel.Composition; + using System.IO; using System.Linq; using System.Threading; @@ -13,7 +13,6 @@ using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; -using MonoDevelop.MSBuild.Editor.Completion; using MonoDevelop.MSBuild.Language; using MonoDevelop.MSBuild.Language.Expressions; using MonoDevelop.MSBuild.Language.Typesystem; @@ -22,16 +21,10 @@ namespace MonoDevelop.MSBuild.Editor.Roslyn { - [Export (typeof (IFunctionTypeProvider))] partial class RoslynFunctionTypeProvider : IFunctionTypeProvider { readonly ILogger logger; - [ImportingConstructor] - public RoslynFunctionTypeProvider ([Import (AllowDefault = true)] IRoslynCompilationProvider assemblyLoader, MSBuildEnvironmentLogger environmentLogger) - : this (assemblyLoader, environmentLogger.Logger) - { } - public RoslynFunctionTypeProvider (IRoslynCompilationProvider assemblyLoader, ILogger logger) { AssemblyLoader = assemblyLoader; @@ -40,9 +33,9 @@ public RoslynFunctionTypeProvider (IRoslynCompilationProvider assemblyLoader, IL public IRoslynCompilationProvider AssemblyLoader { get; } - readonly object locker = new object (); - Compilation compilation; - Task compilationLoadTask; + readonly object locker = new(); + Compilation? compilation; + Task? compilationLoadTask; //we need the reference assembly to get docs static string GetMscorlibReferenceAssembly () @@ -94,7 +87,7 @@ public Task EnsureInitialized (CancellationToken token) return Task.WhenAny (compilationLoadTask, Task.Delay (-1, token)); } - public IEnumerable GetPropertyFunctionNameCompletions (ExpressionNode triggerExpression) + public IEnumerable? GetPropertyFunctionNameCompletions (ExpressionNode triggerExpression) { if (triggerExpression is ConcatExpression expression) { triggerExpression = expression.Nodes.Last (); @@ -240,7 +233,7 @@ IEnumerable GetInstanceFunctions (MSBuildValueKind kind, bool incl bool isArray = (kind & MSBuildValueKind.ListSemicolonOrComma) != 0; kind = kind.WithoutModifiers (); - ITypeSymbol type = null; + ITypeSymbol? type = null; if (DotNetTypeMap.FromValueKind (kind) is string dotNetType) { type = compilation?.GetTypeByMetadataName (dotNetType); diff --git a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynHelpers.cs b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynHelpers.cs similarity index 73% rename from MonoDevelop.MSBuild.Editor/Roslyn/RoslynHelpers.cs rename to MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynHelpers.cs index 8919fc91..7f8d47a5 100644 --- a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynHelpers.cs +++ b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynHelpers.cs @@ -20,10 +20,10 @@ public static string GetFullName (this ITypeSymbol symbol) return sb.ToString (); } - // loading the docs from roslyn can be expensive, return a null string and the symbol - // this mean callers have to resolver the docs from the symbol themselves. it's a lot - // simpler to posh the async logic to the callers than to make all the BaseInfo.Description + // loading the docs from roslyn can be expensive, return an empty string and the symbol + // this mean callers have to resolve the docs from the symbol themselves. it's a lot + // simpler to push the async logic to the callers than to make all the BaseInfo.Description // implementations and usages async - public static DisplayText GetDescription (ISymbol symbol) => new DisplayText (null, symbol); + public static DisplayText GetDescription (ISymbol symbol) => new DisplayText ("", symbol); } } diff --git a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynTaskMetadataBuilder.cs b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynTaskMetadataBuilder.cs similarity index 99% rename from MonoDevelop.MSBuild.Editor/Roslyn/RoslynTaskMetadataBuilder.cs rename to MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynTaskMetadataBuilder.cs index bdd436b0..42671434 100644 --- a/MonoDevelop.MSBuild.Editor/Roslyn/RoslynTaskMetadataBuilder.cs +++ b/MonoDevelop.MSBuild.Editor.Common/Roslyn/RoslynTaskMetadataBuilder.cs @@ -45,7 +45,7 @@ public TaskMetadataBuilder (IRoslynCompilationProvider compilationProvider) public TaskInfo? CreateTaskInfo ( string typeName, string? assemblyName, ExpressionNode assemblyFile, string assemblyFileStr, - string declaredInFile, int declaredAtOffset, + string declaredInFile, Xml.Dom.TextSpan? declarationSpan, IMSBuildEvaluationContext evaluationContext, ILogger logger) { @@ -104,7 +104,7 @@ public TaskMetadataBuilder (IRoslynCompilationProvider compilationProvider) TaskDeclarationKind.Assembly, type.GetFullName (), assemblyName, assemblyFileStr, - declaredInFile, declaredAtOffset, + declaredInFile, declarationSpan, versionInfo, parameters); } diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/MonoDevelop.MSBuild.Editor.VisualStudio.csproj b/MonoDevelop.MSBuild.Editor.VisualStudio/MonoDevelop.MSBuild.Editor.VisualStudio.csproj index 21e0770d..9d69daf2 100644 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/MonoDevelop.MSBuild.Editor.VisualStudio.csproj +++ b/MonoDevelop.MSBuild.Editor.VisualStudio/MonoDevelop.MSBuild.Editor.VisualStudio.csproj @@ -14,6 +14,12 @@ false true + + + false + false + + Program @@ -35,9 +41,8 @@ - - - + + diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.InterpolatedStringHandlers.cs b/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.InterpolatedStringHandlers.cs deleted file mode 100644 index 386eace7..00000000 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.InterpolatedStringHandlers.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable enable annotations - -using System.Runtime.CompilerServices; -using System.Text; - -#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/58168 - -namespace Roslyn.Utilities -{ - internal static partial class Contract - { - [InterpolatedStringHandler] - public readonly struct ThrowIfTrueInterpolatedStringHandler - { - private readonly StringBuilder _stringBuilder; - - public ThrowIfTrueInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool success) - { - _stringBuilder = condition ? new StringBuilder(capacity: literalLength) : null!; - success = condition; - } - - public void AppendLiteral(string value) => _stringBuilder.Append(value); - - public void AppendFormatted(T value) => _stringBuilder.Append(value?.ToString()); - - public string GetFormattedText() => _stringBuilder.ToString(); - } - - [InterpolatedStringHandler] - public readonly struct ThrowIfFalseInterpolatedStringHandler - { - private readonly StringBuilder _stringBuilder; - - public ThrowIfFalseInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool success) - { - _stringBuilder = condition ? null! : new StringBuilder(capacity: literalLength); - success = !condition; - } - - public void AppendLiteral(string value) => _stringBuilder.Append(value); - - public void AppendFormatted(T value) => _stringBuilder.Append(value?.ToString()); - - public string GetFormattedText() => _stringBuilder.ToString(); - } - - [InterpolatedStringHandler] - public readonly struct ThrowIfNullInterpolatedStringHandler - { - private readonly StringBuilder _stringBuilder; - - public ThrowIfNullInterpolatedStringHandler(int literalLength, int formattedCount, T? value, out bool success) - { - _stringBuilder = value is null ? new StringBuilder(capacity: literalLength) : null!; - success = value is null; - } - - public void AppendLiteral(string value) => _stringBuilder.Append(value); - - public void AppendFormatted(T2 value) => _stringBuilder.Append(value?.ToString()); - - public string GetFormattedText() => _stringBuilder.ToString(); - } - } -} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.cs b/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.cs deleted file mode 100644 index 5b8a4968..00000000 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/Contract.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable enable annotations - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Roslyn.Utilities -{ - internal static partial class Contract - { - // Guidance on inlining: - // ThrowXxx methods are used heavily across the code base. - // Inline their implementation of condition checking but don't inline the code that is only executed on failure. - // This approach makes the common path efficient (both execution time and code size) - // while keeping the rarely executed code in a separate method. - - /// - /// Throws a non-accessible exception if the provided value is null. This method executes in - /// all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int lineNumber = 0) where T : class? - { - if (value is null) - { - Fail("Unexpected null", lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is null. This method executes in - /// all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lineNumber = 0) where T : struct - { - if (value is null) - { - Fail("Unexpected null", lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is null. This method executes in - /// all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, string message, [CallerLineNumber] int lineNumber = 0) - { - if (value is null) - { - Fail(message, lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is null. This method executes in - /// all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerArgument("value")] ThrowIfNullInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) - { - if (value is null) - { - Fail(message.GetFormattedText(), lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is false. This method executes - /// in all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [CallerLineNumber] int lineNumber = 0) - { - if (!condition) - { - Fail("Unexpected false", lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is false. This method executes - /// in all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) - { - if (!condition) - { - Fail(message, lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is false. This method executes - /// in all builds - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfFalseInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) - { - if (!condition) - { - Fail(message.GetFormattedText(), lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is true. This method executes in - /// all builds. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [CallerLineNumber] int lineNumber = 0) - { - if (condition) - { - Fail("Unexpected true", lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is true. This method executes in - /// all builds. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) - { - if (condition) - { - Fail(message, lineNumber); - } - } - - /// - /// Throws a non-accessible exception if the provided value is true. This method executes in - /// all builds. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfTrueInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) - { - if (condition) - { - Fail(message.GetFormattedText(), lineNumber); - } - } - - [DebuggerHidden] - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void Fail(string message = "Unexpected", [CallerLineNumber] int lineNumber = 0) - => throw new InvalidOperationException($"{message} - line {lineNumber}"); - } -} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerArgumentAttribute.cs b/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerArgumentAttribute.cs deleted file mode 100644 index a3069caa..00000000 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerArgumentAttribute.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if NET6_0_OR_GREATER - -using System.Runtime.CompilerServices; - -#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) -[assembly: TypeForwardedTo(typeof(InterpolatedStringHandlerArgumentAttribute))] -#pragma warning restore RS0016 // Add public types and members to the declared API - -#else - -namespace System.Runtime.CompilerServices -{ - /// Indicates which arguments to a method involving an interpolated string handler should be passed to that handler. - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute - { - /// Initializes a new instance of the class. - /// The name of the argument that should be passed to the handler. - /// may be used as the name of the receiver in an instance method. - public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = new string[] { argument }; - /// Initializes a new instance of the class. - /// The names of the arguments that should be passed to the handler. - /// may be used as the name of the receiver in an instance method. - public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => Arguments = arguments; - /// Gets the names of the arguments that should be passed to the handler. - /// may be used as the name of the receiver in an instance method. - public string[] Arguments { get; } - } -} - -#endif \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerAttribute.cs b/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerAttribute.cs deleted file mode 100644 index cf6f6fce..00000000 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/RoslynImport/Helpers/InterpolatedStringHandlerAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if NET6_0_OR_GREATER - -using System.Runtime.CompilerServices; - -#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) -[assembly: TypeForwardedTo(typeof(InterpolatedStringHandlerAttribute))] -#pragma warning restore RS0016 // Add public types and members to the declared API - -#else - -namespace System.Runtime.CompilerServices -{ - /// Indicates the attributed type is to be used as an interpolated string handler. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] - internal sealed class InterpolatedStringHandlerAttribute : Attribute - { - /// Initializes the . - public InterpolatedStringHandlerAttribute() { } - } -} - -#endif \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor.VisualStudio/source.extension.vsixmanifest b/MonoDevelop.MSBuild.Editor.VisualStudio/source.extension.vsixmanifest index 5eb5a7a4..5f32a963 100644 --- a/MonoDevelop.MSBuild.Editor.VisualStudio/source.extension.vsixmanifest +++ b/MonoDevelop.MSBuild.Editor.VisualStudio/source.extension.vsixmanifest @@ -14,10 +14,10 @@ true - + amd64 - + arm64 diff --git a/MonoDevelop.MSBuild.Editor/Analysis/EditTextActionOperation.cs b/MonoDevelop.MSBuild.Editor/Analysis/EditTextActionOperation.cs index 4b7f1b5f..56617417 100644 --- a/MonoDevelop.MSBuild.Editor/Analysis/EditTextActionOperation.cs +++ b/MonoDevelop.MSBuild.Editor/Analysis/EditTextActionOperation.cs @@ -13,7 +13,6 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; -using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Dom; namespace MonoDevelop.MSBuild.Editor.Analysis @@ -188,8 +187,8 @@ public EditTextActionOperation Insert (int offset, string text, TextSpan[]? rela => WithEdit (new Edit (EditKind.Insert, new TextSpan (offset, 0), text, relativeSelections, baseIndentDepth)); public EditTextActionOperation InsertAndSelect (int offset, string text, TextSpan[]? relativeSelections = null, int baseIndentDepth = 0) => WithEdit (new Edit (EditKind.Insert, new TextSpan (offset, 0), text, relativeSelections ?? new[] { new TextSpan (0, text.Length) }, baseIndentDepth)); - public EditTextActionOperation InsertAndSelect (int offset, string text, char selectionMarker, int baseIndentDepth = 0) - => WithEdit (Edit.WithMarkedSelection (EditKind.Insert, new TextSpan (offset, 0), text, selectionMarker, baseIndentDepth)); + public EditTextActionOperation InsertAndSelect (int offset, string textWithMarkers, char selectionMarker, int baseIndentDepth = 0) + => WithEdit (Edit.WithMarkedSelection (EditKind.Insert, new TextSpan (offset, 0), textWithMarkers, selectionMarker, baseIndentDepth)); public EditTextActionOperation Replace (int offset, int length, string text, int baseIndentDepth = 0) => WithEdit (new Edit (EditKind.Replace, new TextSpan (offset, length), text)); @@ -201,10 +200,10 @@ public EditTextActionOperation ReplaceAndSelect (int offset, int length, string public EditTextActionOperation ReplaceAndSelect (TextSpan span, string text, TextSpan[]? relativeSelections = null, int baseIndentDepth = 0) => WithEdit (new Edit (EditKind.Replace, span, text, relativeSelections ?? new[] { new TextSpan (0, text.Length) }, baseIndentDepth)); - public EditTextActionOperation ReplaceAndSelect (int offset, int length, string text, char selectionMarker, int baseIndentDepth = 0) - => WithEdit (Edit.WithMarkedSelection (EditKind.Replace, new TextSpan (offset, length), text, selectionMarker, baseIndentDepth)); - public EditTextActionOperation ReplaceAndSelect (TextSpan span, string text, char selectionMarker, int baseIndentDepth = 0) - => WithEdit (Edit.WithMarkedSelection (EditKind.Replace, span, text, selectionMarker, baseIndentDepth)); + public EditTextActionOperation ReplaceAndSelect (int offset, int length, string textWithMarkers, char selectionMarker, int baseIndentDepth = 0) + => WithEdit (Edit.WithMarkedSelection (EditKind.Replace, new TextSpan (offset, length), textWithMarkers, selectionMarker, baseIndentDepth)); + public EditTextActionOperation ReplaceAndSelect (TextSpan span, string textWithMarkers, char selectionMarker, int baseIndentDepth = 0) + => WithEdit (Edit.WithMarkedSelection (EditKind.Replace, span, textWithMarkers, selectionMarker, baseIndentDepth)); public EditTextActionOperation Delete (int offset, int length) => WithEdit (new Edit (EditKind.Delete, new TextSpan (offset, length))); @@ -235,11 +234,39 @@ public Edit (EditKind kind, TextSpan span, string? text = null, TextSpan[]? rela BaseIndentDepth = baseIndentDepth; } - public static Edit WithMarkedSelection (EditKind kind, TextSpan span, string text, char selectionMarker, int? baseIndentDepth = null) + public static Edit WithMarkedSelection (EditKind kind, TextSpan span, string textWithMarkers, char selectionMarker, int? baseIndentDepth = null) { - var parsed = TextWithMarkers.Parse (text, selectionMarker); - var relativeSelections = parsed.GetMarkedSpans (); - return new Edit (kind, span, parsed.Text, relativeSelections, baseIndentDepth); + (var text, var relativeSelections) = ExtractSpans (textWithMarkers, selectionMarker); + + return new Edit (kind, span, text, relativeSelections.ToArray (), baseIndentDepth); + } + + static (string text, List selections) ExtractSpans (string textWithMarkers, char selectionMarker) + { + var spans = new List (); + + int spanStart = -1; + var cleanTextBuilder = new StringBuilder (textWithMarkers.Length); + + for (int i = 0; i < textWithMarkers.Length; i++) { + char c = textWithMarkers[i]; + if (c == selectionMarker) { + if (spanStart < 0) { + spanStart = cleanTextBuilder.Length; + } else { + spans.Add (TextSpan.FromBounds (spanStart, cleanTextBuilder.Length)); + spanStart = -1; + } + } else { + cleanTextBuilder.Append (c); + } + } + + if (spanStart > -1) { + throw new ArgumentException ("Odd number of markers"); + } + + return new (cleanTextBuilder.ToString (), spans); } } diff --git a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeAction.cs b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeAction.cs index 34d16796..a9f21918 100644 --- a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeAction.cs +++ b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeAction.cs @@ -29,7 +29,7 @@ public virtual Task> ComputePreviewOpera abstract class SimpleMSBuildCodeAction : MSBuildCodeAction { public sealed override Task> ComputeOperationsAsync (CancellationToken cancellationToken) - => Task.FromResult> (new MSBuildCodeActionOperation[] { CreateOperation () }); + => Task.FromResult> ([CreateOperation ()]); protected abstract MSBuildCodeActionOperation CreateOperation (); } diff --git a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSpellChecker.cs b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSpellChecker.cs index c4e682c4..d8e9c59a 100644 --- a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSpellChecker.cs +++ b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSpellChecker.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Utilities; using MonoDevelop.MSBuild.Language; using MonoDevelop.MSBuild.Language.Typesystem; @@ -15,6 +14,7 @@ using ISymbol = MonoDevelop.MSBuild.Language.ISymbol; using Roslyn.Utilities; +using Microsoft.CodeAnalysis.Shared.Collections; namespace MonoDevelop.MSBuild.Editor.Analysis { @@ -51,10 +51,10 @@ Task GetItemChecker (MSBuildDocument document) CheckHash (document); return itemCheckerTask ??= Task.Run (() => new SpellChecker ( - Checksum.Null, - document.GetSchemas ().GetItems ().Select (i => new StringSlice (i.Name))) + document.GetSchemas ().GetItems ().Select (i => i.Name) ) -; + ); + ; } } @@ -64,10 +64,9 @@ Task GetPropertyChecker (MSBuildDocument document) CheckHash (document); return propertyCheckerTask ??= Task.Run (() => new SpellChecker ( - Checksum.Null, - document.GetSchemas ().GetProperties (true).Select (p => new StringSlice (p.Name))) + document.GetSchemas ().GetProperties (true).Select (p => p.Name) ) -; + ); } } @@ -78,9 +77,9 @@ Task GetMetadataChecker (MSBuildDocument document, string itemName if (!metadataCheckerTasks.TryGetValue (itemName, out var checker)) { metadataCheckerTasks[itemName] = checker = Task.Run (() => new SpellChecker ( - Checksum.Null, - document.GetSchemas ().GetMetadata (itemName, true).Select (p => new StringSlice (p.Name))) - ); + document.GetSchemas ().GetMetadata (itemName, true).Select (p => p.Name) + ) + ); } return checker; } @@ -96,8 +95,7 @@ Task GetValueChecker (MSBuildDocument document, MSBuildValueKind k valueKindCheckerTasks[kind] = checker = Task.Run (() => new SpellChecker ( - Checksum.Null, - knownVals.Select (p => new StringSlice (p.Name))) + knownVals.Select (p => p.Name)) ); } return checker; @@ -108,8 +106,7 @@ Task GetValueChecker (MSBuildDocument document, MSBuildValueKind k customTypeCheckerTasks[customType] = checker = Task.Run (() => { var knownVals = customType.Values; return new SpellChecker ( - Checksum.Null, - knownVals.Select (p => new StringSlice (p.Name)) + knownVals.Select (p => p.Name) ); }); } @@ -122,7 +119,10 @@ public async Task> FindSimilarItems (MSBuildDocument docum IEnumerable GetItems (MSBuildDocument document, SpellChecker checker, string name) { - foreach (var match in checker.FindSimilarWords (name)) { + var matches = TemporaryArray.Empty; + checker.FindSimilarWords (ref matches, name, false); + + foreach (var match in matches) { if (string.Equals (match, name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -137,7 +137,10 @@ public async Task> FindSimilarProperties (MSBuildDocum IEnumerable GetProperties (MSBuildDocument document, SpellChecker checker, string name) { - foreach (var match in checker.FindSimilarWords (name)) { + var matches = TemporaryArray.Empty; + checker.FindSimilarWords (ref matches, name, false); + + foreach (var match in matches) { if (string.Equals (match, name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -152,7 +155,10 @@ public async Task> FindSimilarMetadata (MSBuildDocumen IEnumerable GetMetadata (MSBuildDocument document, SpellChecker checker, string itemName, string name) { - foreach (var match in checker.FindSimilarWords (name)) { + var matches = TemporaryArray.Empty; + checker.FindSimilarWords (ref matches, name, false); + + foreach (var match in matches) { if (string.Equals (match, name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -171,7 +177,11 @@ IEnumerable GetValue (SpellChecker checker, MSBuildValueKind kind, Cust var valueComparer = (customType?.CaseSensitive ?? false) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; var knownValDict = knownVals.ToDictionary (v => v.Name, StringComparer.OrdinalIgnoreCase); - foreach (var match in checker.FindSimilarWords (name)) { + + var matches = TemporaryArray.Empty; + checker.FindSimilarWords (ref matches, name, false); + + foreach (var match in matches) { if (string.Equals (match, name, valueComparer)) { continue; } diff --git a/MonoDevelop.MSBuild.Editor/Completion/MSBuildCompletionSource.cs b/MonoDevelop.MSBuild.Editor/Completion/MSBuildCompletionSource.cs index 0c9aa5b9..9b1e3b1a 100644 --- a/MonoDevelop.MSBuild.Editor/Completion/MSBuildCompletionSource.cs +++ b/MonoDevelop.MSBuild.Editor/Completion/MSBuildCompletionSource.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -57,30 +56,14 @@ protected override Task> GetElementCompletionsAsync (MSBui { var doc = context.Document; - // we can't use the LanguageElement from the resolveresult here. - // if completion is triggered in an existing element's name, the resolveresult - // will be for that element, so completion will be for the element's children - // rather than for the element itself. var nodePath = context.NodePath; - MSBuildElementSyntax languageElement = null; - string elName = null; - for (int i = 1; i < nodePath.Count; i++) { - if (nodePath[i] is XElement el) { - elName = el.Name.Name; - languageElement = MSBuildElementSyntax.Get (elName, languageElement); - continue; - } - return TaskCompleted (null); - } - - // if we don't have a language element and we're not at root level, we're in an invalid location - if (languageElement == null && nodePath.Count > 2) { + if (!CompletionHelpers.TryGetElementSyntaxForElementCompletion(nodePath, out MSBuildElementSyntax languageElement, out string elementName)) { return TaskCompleted (null); } var items = new List (); - foreach (var el in doc.GetElementCompletions (languageElement, elName)) { + foreach (var el in doc.GetElementCompletions (languageElement, elementName)) { if (el is ItemInfo) { items.Add (CreateCompletionItem (context.DocumentationProvider, el, XmlCompletionItemKind.SelfClosingElement, includeBracket ? "<" : null)); } else { @@ -181,8 +164,13 @@ protected override CompletionStartData InitializeCompletion (CompletionTrigger t return baseCompletion; } - string expression = spine.GetIncompleteValue (triggerLocation.Snapshot); - int exprStartPos = triggerLocation - expression.Length; + // TryGetIncompleteValue may return false while still outputting incomplete values, if it fails due to reaching maximum readahead. + // It will also return false and output null values if we're in an element value that only contains whitespace. + // In both these cases we can ignore the false return and proceed anyways. + spineParser.TryGetIncompleteValue (triggerLocation.Snapshot, out var expression, out var valueSpan, cancellationToken: token); + expression ??= ""; + int exprStartPos = valueSpan?.Start ?? triggerLocation; + var triggerState = GetTriggerState ( expression, triggerLocation - exprStartPos, @@ -208,32 +196,33 @@ protected override CompletionStartData InitializeCompletion (CompletionTrigger t protected override async Task> GetAdditionalCompletionsAsync (MSBuildCompletionContext context, CancellationToken token) { - if (context.ResolveResult?.ElementSyntax is null || context.ExpressionTriggerReason == ExpressionTriggerReason.Unknown || !IsPossibleExpressionCompletionContext (context.SpineParser)) { + if (context.ExpressionTriggerReason == ExpressionTriggerReason.Unknown) { return null; } - var triggerLocation = context.TriggerLocation; - string expression = context.SpineParser.GetIncompleteValue (triggerLocation.Snapshot); - int exprStartPos = triggerLocation.Position - expression.Length; - var triggerState = GetTriggerState (expression, triggerLocation - exprStartPos, context.ExpressionTriggerReason, context.Trigger.Character, context.ResolveResult.IsCondition (), - out int spanStart, out int spanLength, out ExpressionNode triggerExpression, out var listKind, out IReadOnlyList comparandVariables, - Logger - ); - spanStart = exprStartPos + spanStart; + var msbuildTrigger = MSBuildCompletionTrigger.TryCreate ( + context.SpineParser, + context.TriggerLocation.Snapshot.GetTextSource(), + context.ExpressionTriggerReason, + context.TriggerLocation, + context.Trigger.Character, + Logger, + provider.FunctionTypeProvider, + context.ResolveResult, token); - if (triggerState == TriggerState.None) { + if (msbuildTrigger is null) { return null; } // used by MSBuildCompletionCommitManager - context.Session.Properties.AddProperty (typeof (TriggerState), triggerState); + context.Session.Properties.AddProperty (typeof (TriggerState), msbuildTrigger.TriggerState); var info = context.ResolveResult.GetElementOrAttributeValueInfo (context.Document); if (info is null || info.ValueKind == MSBuildValueKind.Nothing) { return null; } - return await GetExpressionCompletionsAsync (context, info, triggerState, listKind, spanLength, triggerExpression, comparandVariables, token); + return await GetExpressionCompletionsAsync (context, info, msbuildTrigger, token); } async Task> GetPackageNameCompletions (MSBuildCompletionContext context, string searchQuery, string packageType, CancellationToken token) @@ -274,50 +263,13 @@ void AddItems (FeedKind kind) return items; } - static bool ItemIsInItemGroup (XElement itemEl) => itemEl.Parent is XElement parent && parent.Name.Equals (MSBuildElementSyntax.ItemGroup.Name, true); - - static XElement GetItemGroupItemFromMetadata (MSBuildResolveResult rr) - => rr.ElementSyntax.SyntaxKind switch { - MSBuildSyntaxKind.Item => rr.Element, - MSBuildSyntaxKind.Metadata => rr.Element.Parent is XElement parentEl && ItemIsInItemGroup (parentEl)? parentEl : null, - _ => null - }; - - static XAttribute GetIncludeOrUpdateAttribute (XElement item) - => item.Attributes.FirstOrDefault (att => MSBuildElementSyntax.Item.GetAttribute (att)?.SyntaxKind switch { - MSBuildSyntaxKind.Item_Include => true, - MSBuildSyntaxKind.Item_Update => true, - _ => false - }); - async Task> GetPackageVersionCompletions (MSBuildCompletionContext context, CancellationToken token) { - if (context.ResolveResult is not MSBuildResolveResult rr || GetItemGroupItemFromMetadata (rr) is not XElement itemEl || GetIncludeOrUpdateAttribute (itemEl) is not XAttribute includeAtt) { + if (!PackageCompletion.TryGetPackageVersionSearchJob (context.ResolveResult, context.Document, provider.PackageSearchManager, out var packageSearchJob, out _, out _)) { return null; } - // we can only provide version completions if the item's value type is non-list nugetid - var itemInfo = context.Document.GetSchemas ().GetItem (itemEl.Name.Name); - if (itemInfo == null || !itemInfo.ValueKind.IsKindOrListOfKind (MSBuildValueKind.NuGetID)) { - return null; - } - - var packageType = itemInfo.CustomType?.Values[0].Name; - - var packageId = includeAtt.Value; - if (string.IsNullOrEmpty (packageId)) { - return null; - } - - // check it's a non-list literal value, we can't handle anything else - var expr = ExpressionParser.Parse (packageId, ExpressionOptions.ItemsMetadataAndLists); - if (expr.NodeKind != ExpressionNodeKind.Text) { - return null; - } - - var tfm = context.Document.GetTargetFrameworkNuGetSearchParameter (); - - var results = await provider.PackageSearchManager.SearchPackageVersions (packageId.ToLower (), tfm, packageType).ToTask (token); + var results = await packageSearchJob.ToTask (token); //FIXME should we deduplicate? var items = new List (); @@ -329,64 +281,17 @@ async Task> GetPackageVersionCompletions (MSBuildCompletion return items; } - //FIXME: SDK version completion - //FIXME: enumerate SDKs from NuGet - Task> GetSdkCompletions (MSBuildRootDocument doc, CancellationToken token) - { - return Task.Run (() => { - var items = new List (); - var sdks = new HashSet (); - - foreach (var sdk in doc.Environment.GetRegisteredSdks ()) { - if (sdks.Add (sdk.Name)) { - items.Add (CreateSdkCompletionItem (sdk)); - } - } - - //FIXME we should be able to cache these - doc.Environment.ToolsetProperties.TryGetValue (WellKnownProperties.MSBuildSDKsPath, out var sdksPath); - if (sdksPath != null) { - AddSdksFromDir (sdksPath); - } - - var dotNetSdk = doc.Environment.ResolveSdk (new ("Microsoft.NET.Sdk", null, null), null, null, Logger); - if (dotNetSdk?.Path is string sdkPath) { - string dotNetSdkPath = Path.GetDirectoryName (Path.GetDirectoryName (sdkPath)); - if (sdksPath == null || Path.GetFullPath (dotNetSdkPath) != Path.GetFullPath (sdksPath)) { - AddSdksFromDir (dotNetSdkPath); - } - } - - void AddSdksFromDir (string sdkDir) - { - if (!Directory.Exists (sdkDir)) { - return; - } - foreach (var dir in Directory.GetDirectories (sdkDir)) { - string name = Path.GetFileName (dir); - var targetsFileExists = File.Exists (Path.Combine (dir, "Sdk", "Sdk.targets")); - if (targetsFileExists && sdks.Add (name)) { - items.Add (CreateSdkCompletionItem (new SdkInfo (name, null, Path.Combine (dir, name)))); - } - } - } - - return items; - }, token); - } - async Task> GetExpressionCompletionsAsync ( MSBuildCompletionContext context, - ITypedSymbol valueSymbol, TriggerState triggerState, ListKind listKind, - int triggerLength, ExpressionNode triggerExpression, - IReadOnlyList comparandVariables, + ITypedSymbol valueSymbol, MSBuildCompletionTrigger trigger, CancellationToken token) { var doc = context.Document; - var rr = context.ResolveResult; + var rr = trigger.ResolveResult; var kind = valueSymbol.ValueKind; + var triggerState = trigger.TriggerState; - if (!ValidateListPermitted (listKind, kind)) { + if (!ValidateListPermitted (trigger.ListKind, kind)) { return null; } @@ -408,8 +313,8 @@ async Task> GetExpressionCompletionsAsync ( var items = new List (); - if (comparandVariables != null && isValue) { - foreach (var ci in ExpressionCompletion.GetComparandCompletions (doc, fileSystem, comparandVariables, Logger)) { + if (trigger.ComparandVariables != null && isValue) { + foreach (var ci in ExpressionCompletion.GetComparandCompletions (doc, fileSystem, trigger.ComparandVariables, Logger)) { items.Add (CreateCompletionItem (context.DocumentationProvider, ci, XmlCompletionItemKind.AttributeValue)); } } @@ -417,7 +322,7 @@ async Task> GetExpressionCompletionsAsync ( if (isValue) { switch (kind) { case MSBuildValueKind.NuGetID: - if (triggerExpression is ExpressionText t) { + if (trigger.Expression is ExpressionText t) { var packageType = valueSymbol.CustomType?.Values[0].Name; var packageNameItems = await GetPackageNameCompletions (context, t.Value, packageType, token); if (packageNameItems != null) { @@ -434,7 +339,7 @@ async Task> GetExpressionCompletionsAsync ( } case MSBuildValueKind.Sdk: case MSBuildValueKind.SdkWithVersion: { - var sdkItems = await GetSdkCompletions (doc, token); + var sdkItems = SdkCompletion.GetSdkCompletions (doc, Logger, token).Select(s => CreateSdkCompletionItem (s)); if (sdkItems != null) { items.AddRange (sdkItems); } @@ -454,22 +359,18 @@ async Task> GetExpressionCompletionsAsync ( } //TODO: better metadata support - if (valueSymbol.CustomType != null && valueSymbol.CustomType.Values.Count > 0 && isValue) { - // if it's a list of ints or guids, add an annotation to make it easier to navigate - bool addAnnotation = valueSymbol.CustomType.BaseKind switch { - MSBuildValueKind.Guid => true, - MSBuildValueKind.Int => true, - _ => false - }; + // NOTE: can't just check CustomTypeInfo isn't null, must check kind, as NuGetID stashes the dependency type in the CustomTypeInfo + if (kind == MSBuildValueKind.CustomType && valueSymbol.CustomType != null && valueSymbol.CustomType.Values.Count > 0 && isValue) { + bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions (valueSymbol); foreach (var value in valueSymbol.CustomType.Values) { - items.Add (CreateCompletionItem (context.DocumentationProvider, value, XmlCompletionItemKind.AttributeValue, annotation: addAnnotation? value.Description.Text : null)); + items.Add (CreateCompletionItem (context.DocumentationProvider, value, XmlCompletionItemKind.AttributeValue, addDescriptionHint: addDescriptionHint)); } } else { //FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards await provider.FunctionTypeProvider.EnsureInitialized (token); - if (GetCompletionInfos (rr, triggerState, valueSymbol, triggerExpression, triggerLength, doc, provider.FunctionTypeProvider, fileSystem, Logger, kindIfUnknown: kind) is IEnumerable completionInfos) { - bool addDescriptionHint = valueSymbol.IsKindOrDerived (MSBuildValueKind.WarningCode); + if (GetCompletionInfos (rr, triggerState, valueSymbol, trigger.Expression, trigger.SpanLength, doc, provider.FunctionTypeProvider, fileSystem, Logger, kindIfUnknown: kind) is IEnumerable completionInfos) { + bool addDescriptionHint = CompletionHelpers.ShouldAddHintForCompletions (valueSymbol); foreach (var ci in completionInfos) { items.Add (CreateCompletionItem (context.DocumentationProvider, ci, XmlCompletionItemKind.AttributeValue, addDescriptionHint: addDescriptionHint)); } @@ -482,7 +383,7 @@ async Task> GetExpressionCompletionsAsync ( if (allowExpressions && isValue) { items.Add (CreateSpecialItem ("@(", "Item reference", KnownImages.MSBuildItem, MSBuildCommitItemKind.ItemReference)); - if (MSBuildCompletionSource.IsMetadataAllowed (triggerExpression, rr)) { + if (CompletionHelpers.IsMetadataAllowed (trigger.Expression, rr)) { items.Add (CreateSpecialItem ("%(", "Metadata reference", KnownImages.MSBuildItem, MSBuildCommitItemKind.MetadataReference)); } } @@ -490,46 +391,6 @@ async Task> GetExpressionCompletionsAsync ( return items; } - //FIXME: improve logic for determining where metadata is permitted - static bool IsMetadataAllowed (ExpressionNode triggerExpression, MSBuildResolveResult rr) - { - //if any a parent node is an item transform or function, metadata is allowed - if (triggerExpression != null) { - var node = triggerExpression.Find (triggerExpression.Length); - while (node != null) { - if (node is ExpressionItemTransform || node is ExpressionItemFunctionInvocation) { - return true; - } - node = node.Parent; - } - } - - if (rr.AttributeSyntax != null) { - switch (rr.AttributeSyntax.SyntaxKind) { - // metadata attributes on items can refer to other metadata on the items - case MSBuildSyntaxKind.Item_Metadata: - // task params can refer to metadata in batched items - case MSBuildSyntaxKind.Task_Parameter: - // target inputs and outputs can use metadata from each other's items - case MSBuildSyntaxKind.Target_Inputs: - case MSBuildSyntaxKind.Target_Outputs: - return true; - //conditions on metadata elements can refer to metadata on the items - case MSBuildSyntaxKind.Metadata_Condition: - return true; - } - } - - if (rr.ElementSyntax != null) { - switch (rr.ElementSyntax.SyntaxKind) { - // metadata elements can refer to other metadata in the items - case MSBuildSyntaxKind.Metadata: - return true; - } - } - return false; - } - CompletionItem CreateSpecialItem (string text, string description, KnownImages image, MSBuildCommitItemKind kind) { var item = new CompletionItem (text, this, provider.DisplayElementFactory.GetImageElement (image)); @@ -611,4 +472,5 @@ enum MSBuildCommitItemKind ItemReference, MetadataReference } + } diff --git a/MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs b/MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs index 1e3ea0a1..46c182ad 100644 --- a/MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs +++ b/MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs @@ -566,7 +566,7 @@ void AddUrlElement (string url, string linkText) => stackedElements.Add ( static ClassifiedTextRun CreateWebLink (string url, string linkText) => new ClassifiedTextRun ("navigableSymbol", linkText, () => Process.Start (url), url); - public object GetDiagnosticTooltip (MSBuildDiagnostic diagnostic) => GetDiagnosticElement (diagnostic.Descriptor.Severity, diagnostic.GetFormattedMessage () ?? diagnostic.GetFormattedTitle ()); + public object GetDiagnosticTooltip (MSBuildDiagnostic diagnostic) => GetDiagnosticElement (diagnostic.Descriptor.Severity, diagnostic.GetFormattedMessageWithTitle ()); ContainerElement GetDiagnosticElement (MSBuildDiagnosticSeverity severity, string message) { diff --git a/MonoDevelop.MSBuild.Editor/ExportedRoslynFunctionTypeProvider.cs b/MonoDevelop.MSBuild.Editor/ExportedRoslynFunctionTypeProvider.cs new file mode 100644 index 00000000..79d65338 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/ExportedRoslynFunctionTypeProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel.Composition; + +using MonoDevelop.MSBuild.Language; + +namespace MonoDevelop.MSBuild.Editor.Roslyn; + +[Export (typeof (IFunctionTypeProvider))] +class ExportedRoslynFunctionTypeProvider : RoslynFunctionTypeProvider +{ + [ImportingConstructor] + public ExportedRoslynFunctionTypeProvider (IRoslynCompilationProvider assemblyLoader, MSBuildEnvironmentLogger environmentLogger) + : base (assemblyLoader, environmentLogger.Logger) + { + } +} diff --git a/MonoDevelop.MSBuild.Editor/Exports/.editorconfig b/MonoDevelop.MSBuild.Editor/Exports/.editorconfig new file mode 100644 index 00000000..1fe8c7a3 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: http://EditorConfig.org + +[*.cs] + +# revert settings to match roslyn style better +indent_style = space +trim_trailing_whitespace = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_keywords_in_control_flow_statements = false + +# Newline settings +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false + +# VS threading analyzer triggers on imported roslyn code +dotnet_diagnostic.VSTHRD002.severity = none +dotnet_diagnostic.VSTHRD003.severity = none +dotnet_diagnostic.VSTHRD103.severity = none +dotnet_diagnostic.VSTHRD110.severity = none \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedFileSystem.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedFileSystem.cs new file mode 100644 index 00000000..6ec1069c --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedFileSystem.cs @@ -0,0 +1,14 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IFileSystem))] +[Name("Default File System Implementation")] +internal class ExportedFileSystem : FileSystem +{ +} diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetDiskFeedFactory.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetDiskFeedFactory.cs new file mode 100644 index 00000000..f695b27b --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetDiskFeedFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds.Disk; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IPackageFeedFactory))] +[Name("Default Package Feed Factory")] +internal class ExportedNuGetDiskFeedFactory : NuGetDiskFeedFactory +{ + [ImportingConstructor] + public ExportedNuGetDiskFeedFactory(IFileSystem fileSystem) + : base(fileSystem) + { + } +} diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetV3ServiceFeedFactory.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetV3ServiceFeedFactory.cs new file mode 100644 index 00000000..057d4b4f --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedNuGetV3ServiceFeedFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds.Web; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IPackageFeedFactory))] +[Name("Default NuGet v3 Service Feed Factory")] +internal class ExportedNuGetV3ServiceFeedFactory : NuGetV3ServiceFeedFactory +{ + [ImportingConstructor] + public ExportedNuGetV3ServiceFeedFactory(IWebRequestFactory webRequestFactory) + : base(webRequestFactory) + { + } +} diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageFeedFactorySelector.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageFeedFactorySelector.cs new file mode 100644 index 00000000..c468432f --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageFeedFactorySelector.cs @@ -0,0 +1,21 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IPackageFeedFactorySelector))] +[Name("Default Package Feed Factory Selector")] +internal class ExportedPackageFeedFactorySelector : PackageFeedFactorySelector +{ + [ImportingConstructor] + public ExportedPackageFeedFactorySelector([ImportMany] IEnumerable feedFactories) + : base(feedFactories) + { + } +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageSearchManager.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageSearchManager.cs new file mode 100644 index 00000000..f7cffc53 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedPackageSearchManager.cs @@ -0,0 +1,20 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Search; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IPackageSearchManager))] +[Name("Default Package Search Manager")] +internal class ExportedPackageSearchManager : PackageSearchManager +{ + [ImportingConstructor] + public ExportedPackageSearchManager(IPackageFeedRegistryProvider feedRegistry, IPackageFeedFactorySelector factorySelector) + : base(feedRegistry, factorySelector) + { + } +} diff --git a/MonoDevelop.MSBuild.Editor/Exports/ExportedWebRequestFactory.cs b/MonoDevelop.MSBuild.Editor/Exports/ExportedWebRequestFactory.cs new file mode 100644 index 00000000..8644379c --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/Exports/ExportedWebRequestFactory.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.Exports; + +[Export(typeof(IWebRequestFactory))] +[Name("Default Web Request Factory")] +internal class ExportedWebRequestFactory : WebRequestFactory +{ +} diff --git a/MonoDevelop.MSBuild.Editor/MSBuildEnvironmentLogger.cs b/MonoDevelop.MSBuild.Editor/MSBuildEnvironmentLogger.cs index b470abaf..16957149 100644 --- a/MonoDevelop.MSBuild.Editor/MSBuildEnvironmentLogger.cs +++ b/MonoDevelop.MSBuild.Editor/MSBuildEnvironmentLogger.cs @@ -7,7 +7,7 @@ using MonoDevelop.Xml.Editor.Logging; -namespace MonoDevelop.MSBuild.Editor.Completion +namespace MonoDevelop.MSBuild.Editor { /// /// Shared logger any component can import diff --git a/MonoDevelop.MSBuild.Editor/MonoDevelop.MSBuild.Editor.csproj b/MonoDevelop.MSBuild.Editor/MonoDevelop.MSBuild.Editor.csproj index 50957689..58f635c7 100644 --- a/MonoDevelop.MSBuild.Editor/MonoDevelop.MSBuild.Editor.csproj +++ b/MonoDevelop.MSBuild.Editor/MonoDevelop.MSBuild.Editor.csproj @@ -1,32 +1,25 @@ - net48;net8.0 + net48 True True $(NoWarn);1591;1573 - - - - - - - - - - - + + + + diff --git a/MonoDevelop.MSBuild.Editor/Navigation/MSBuildNavigationService.cs b/MonoDevelop.MSBuild.Editor/Navigation/MSBuildNavigationService.cs index 3ce4074d..2e7eeec2 100644 --- a/MonoDevelop.MSBuild.Editor/Navigation/MSBuildNavigationService.cs +++ b/MonoDevelop.MSBuild.Editor/Navigation/MSBuildNavigationService.cs @@ -133,7 +133,7 @@ public bool Navigate (MSBuildNavigationResult result, ITextBuffer buffer) } if (result.DestFile != null) { - EditorHost.OpenFile (result.DestFile, result.DestOffset); + EditorHost.OpenFile (result.DestFile, result.TargetSpan?.Start ?? 0); return true; } @@ -180,7 +180,7 @@ async Task ShowMultipleFiles (string[] files, ITextBuffer buffer, ILogger logger lineText = buf.CurrentSnapshot.GetLineFromPosition (0).GetText (); } catch (Exception ex) { - LogErrorGettingFileText (logger, ex, file); + MSBuildNavigationHelpers.LogErrorGettingFileText (logger, ex, file); continue; } var classifiedSpans = ImmutableArray.Empty; @@ -228,31 +228,9 @@ async Task FindReferencesAsync (ITextBuffer buffer, MSBuildResolveResult referen { var referenceName = reference.GetReferenceDisplayName (); - string searchTitle = reference.ReferenceKind switch { - MSBuildReferenceKind.Item => $"Item '{referenceName}' references", - MSBuildReferenceKind.Property => $"Property '{referenceName}' references", - MSBuildReferenceKind.Metadata => $"Metadata '{referenceName}' references", - MSBuildReferenceKind.Task => $"Task '{referenceName}' references", - MSBuildReferenceKind.TaskParameter => $"Task parameter '{referenceName}' references", - MSBuildReferenceKind.Keyword => $"Keyword '{referenceName}' references", - MSBuildReferenceKind.Target => $"Target '{referenceName}' references", - MSBuildReferenceKind.KnownValue => $"Value '{referenceName}' references", - MSBuildReferenceKind.NuGetID => $"NuGet package '{referenceName}' references", - MSBuildReferenceKind.TargetFramework => $"Target framework '{referenceName}' references", - MSBuildReferenceKind.ItemFunction => $"Item function '{referenceName}' references", - MSBuildReferenceKind.PropertyFunction => $"Property function '{referenceName}' references", - MSBuildReferenceKind.StaticPropertyFunction => $"Static '{referenceName}' references", - MSBuildReferenceKind.ClassName => $"Class '{referenceName}' references", - MSBuildReferenceKind.Enum => $"Enum '{referenceName}' references", - MSBuildReferenceKind.ConditionFunction => $"Condition function '{referenceName}' references", - MSBuildReferenceKind.FileOrFolder => $"Path '{referenceName}' references", - MSBuildReferenceKind.TargetFrameworkIdentifier => $"TargetFrameworkIdentifier '{referenceName}' references", - MSBuildReferenceKind.TargetFrameworkVersion => $"TargetFrameworkVersion '{referenceName}' references", - MSBuildReferenceKind.TargetFrameworkProfile => $"TargetFrameworkProfile '{referenceName}' references", - _ => logger.LogUnhandledCaseAndReturnDefaultValue ($"'{referenceName}' references", reference.ReferenceKind) - }; - - var searchCtx = Presenter.StartSearch ($"'{referenceName}' references", referenceName, true); + string searchTitle = MSBuildNavigationHelpers.GetFindReferencesSearchTitle (reference, logger); + + var searchCtx = Presenter.StartSearch (searchTitle, referenceName, true); try { await FindReferences (searchCtx, (doc, text, logger, reporter) => MSBuildReferenceCollector.Create (doc, text, logger, reference, Resolver.FunctionTypeProvider, reporter), buffer); @@ -264,7 +242,8 @@ async Task FindReferencesAsync (ITextBuffer buffer, MSBuildResolveResult referen async Task FindTargetDefinitions (string targetName, ITextBuffer buffer) { - var searchCtx = Presenter.StartSearch ($"Target '{targetName}' definitions", targetName, true); + var title = MSBuildNavigationHelpers.GetFindTargetDefinitionsSearchTitle (targetName); + var searchCtx = Presenter.StartSearch (title, targetName, true); try { await FindReferences (searchCtx, (doc, text, logger, reporter) => new MSBuildTargetDefinitionCollector (doc, text, logger, targetName, reporter), buffer); @@ -277,17 +256,15 @@ async Task FindTargetDefinitions (string targetName, ITextBuffer buffer) async Task FindPropertyWrites (string propertyName, ITextBuffer buffer) { - var searchCtx = Presenter.StartSearch ($"Property '{propertyName}' writes", propertyName, true); + var title = MSBuildNavigationHelpers.GetFindPropertyWritesSearchTitle(propertyName); + var searchCtx = Presenter.StartSearch (title, propertyName, true); try { await FindReferences ( searchCtx, (doc, text, logger, reporter) => new MSBuildPropertyReferenceCollector (doc, text, logger, propertyName, reporter), buffer, - result => result.Usage switch { - ReferenceUsage.Declaration or ReferenceUsage.Write => true, - _ => false - }); + MSBuildNavigationHelpers.FilterUsageWrites); } catch (Exception ex) when (!(ex is OperationCanceledException && searchCtx.CancellationToken.IsCancellationRequested)) { var logger = LoggerService.GetLogger (buffer); LogErrorFindReferences (logger, ex); @@ -297,17 +274,15 @@ await FindReferences ( async Task FindItemWrites (string itemName, ITextBuffer buffer) { - var searchCtx = Presenter.StartSearch ($"Item '{itemName}' item", itemName, true); + var title = MSBuildNavigationHelpers.GetFindTargetDefinitionsSearchTitle (itemName); + var searchCtx = Presenter.StartSearch (title, itemName, true); try { await FindReferences ( searchCtx, (doc, text, logger, reporter) => new MSBuildItemReferenceCollector (doc, text, logger, itemName, reporter), buffer, - result => result.Usage switch { - ReferenceUsage.Declaration or ReferenceUsage.Write => true, - _ => false - }); + MSBuildNavigationHelpers.FilterUsageWrites); } catch (Exception ex) when (!(ex is OperationCanceledException && searchCtx.CancellationToken.IsCancellationRequested)) { var logger = LoggerService.GetLogger (buffer); LogErrorFindReferences (logger, ex); @@ -315,14 +290,12 @@ await FindReferences ( await searchCtx.OnCompletedAsync (); } - delegate MSBuildReferenceCollector ReferenceCollectorFactory (MSBuildDocument doc, ITextSource textSource, ILogger logger, FindReferencesReporter reportResult); - /// /// this does not need a cancellation token because it creates UI that handles cancellation /// async Task FindReferences ( FindReferencesContext searchCtx, - ReferenceCollectorFactory collectorFactory, + MSBuildReferenceCollectorFactory collectorFactory, ITextBuffer buffer, Func? resultFilter = null) { @@ -332,16 +305,16 @@ async Task FindReferences ( var parser = ParserProvider.GetParser (buffer); var r = await parser.GetOrProcessAsync (buffer.CurrentSnapshot, searchCtx.CancellationToken); - var doc = r.MSBuildDocument; + var originDoc = r.MSBuildDocument; var logger = LoggerService.GetLogger (buffer); - var jobs = doc.GetDescendentImports () + var jobs = originDoc.GetDescendentImports () .Where (imp => imp.IsResolved) .Select (imp => new FindReferencesSearchJob (imp.Filename, null, null)) - .Prepend (new FindReferencesSearchJob (doc.Filename, doc.XDocument, doc.Text as SnapshotTextSource)) + .Prepend (new FindReferencesSearchJob (originDoc.Filename, originDoc.XDocument, originDoc.Text as SnapshotTextSource)) .ToList (); - int jobsCompleted = jobs.Count; + int jobsCompleted = 0; await ParallelAsync.ForEach (jobs, Environment.ProcessorCount, async (job, token) => { try { @@ -359,7 +332,9 @@ await ParallelAsync.ForEach (jobs, Environment.ProcessorCount, async (job, token token.ThrowIfCancellationRequested (); - var collector = collectorFactory (doc, job.TextSource, logger, ReportResult); + // the collector only uses the MSBuildDocument to resolve schemas, + // so we can use the root document here. + var collector = collectorFactory (originDoc, job.TextSource, logger, ReportResult); collector.Run (job.Document.RootElement); var progress = Interlocked.Increment (ref jobsCompleted); @@ -394,23 +369,17 @@ void ReportResult (FindReferencesResult result) } } catch (Exception ex) { - LogErrorSearchingFile (logger, ex, job.Filename); + MSBuildNavigationHelpers.LogErrorSearchingFile (logger, ex, job.Filename); } }, searchCtx.CancellationToken); } - [LoggerMessage (EventId = 0, Level = LogLevel.Warning, Message = "Error searching for references in MSBuild file '{filename}'")] - static partial void LogErrorSearchingFile (ILogger logger, Exception ex, UserIdentifiableFileName filename); - - [LoggerMessage (EventId = 1, Level = LogLevel.Error, Message = "Unhandled error in Find References'")] + [LoggerMessage (EventId = 0, Level = LogLevel.Error, Message = "Unhandled error in Find References'")] static partial void LogErrorFindReferences (ILogger logger, Exception ex); - [LoggerMessage (EventId = 2, Level = LogLevel.Error, Message = "Unhandled error navigating to multiple files")] + [LoggerMessage (EventId = 1, Level = LogLevel.Error, Message = "Unhandled error navigating to multiple files")] static partial void LogErrorShowingNavigateMultiple (ILogger logger, Exception ex); - [LoggerMessage (EventId = 3, Level = LogLevel.Error, Message = "Error getting text for file '{filename}'")] - static partial void LogErrorGettingFileText (ILogger logger, Exception ex, UserIdentifiableFileName filename); - class FindReferencesSearchJob { public FindReferencesSearchJob (string filename, XDocument document, SnapshotTextSource textSource) diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Builder.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Builder.cs deleted file mode 100644 index 94b7b210..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Builder.cs +++ /dev/null @@ -1,294 +0,0 @@ -// 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.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Utilities; - -namespace Roslyn.Utilities -{ - internal partial class BKTree - { - private class Builder - { - // The number of edges we pre-allocate space for for each node in _compactEdges. - // - // To make the comments simpler below, i'll use '4' as a synonym for CompactEdgeAllocationSize. - // '4' simply reads better and makes it clearer what's going on. - private const int CompactEdgeAllocationSize = 4; - - // Instead of producing a char[] for each string we're building a node for, we instead - // have one long char[] with all the chracters of each string concatenated. i.e. - // "goo" "bar" and "baz" becomes { f, o, o, b, a, r, b, a, z }. Then in _wordSpans - // we have the text spans for each of those words in this array. This gives us only - // two allocations instead of as many allocations as the number of strings we have. - // - // Once we are done building, we pass this to the BKTree and its nodes also state the - // span of this array that corresponds to the word they were created for. This works - // well as other dependent facilities (like EditDistance) can work on sub-arrays without - // any problems. - private readonly char[] _concatenatedLowerCaseWords; - private readonly TextSpan[] _wordSpans; - - // Note: while building a BKTree we have to store children with parents, keyed by the - // edit distance between the two. Naive implementations might store a list or dictionary - // of children along with each node. However, this would be very inefficient and would - // put an enormous amount of memory pressure on the system. - // - // Emperical data for a nice large assembly like mscorlib gives us the following - // information: - // - // Unique-Words (ignoring case): 9662 - // - // For each unique word we need a node in the BKTree. If we stored a list or dictionary - // with each node, that would be 10s of thousands of objects created that would then - // just have to be GCed. That's a lot of garbage pressure we'd like to avoid. - // - // Now if we look at all those nodes, we can see the following information about how many - // children each has. - // - // Edge counts: - // 0 5560 - // 1 1884 - // 2 887 - // 3 527 - // 4 322 - // 5 200 - // 6 114 - // 7 69 - // 8 47 - // 9 20 - // 10 8 - // 11 10 - // 12 7 - // 13 4 - // 15 1 - // 16 1 - // 54 1 - // - // - // i.e. The number of nodes with edge-counts less than or equal to four is: 5560+1884+887+527+322=9180. - // This is 95% of the total number of edges we are adding. Looking at many other dlls - // we found that this ratio stays true across the board. i.e. with all dlls, 95% of nodes - // have 4 or less edges. - // - // So, to optimize things, we pre-alloc a single array with space for 4 edges for each - // node we're going to add. Each node then gets that much space to store edge information. - // If it needs more than that space, then we have a fall-over dictionary that it can store - // information in. - // - // Once building is complete, the GC only needs to deallocate this single array and the - // spillover dictionaries. - // - // This approach produces 1/20th the amount of garbage while building the tree. - // - // Each node at index i has its edges in this array in the range [4*i, 4*i + 4); - private readonly Edge[] _compactEdges; - private readonly BuilderNode[] _builderNodes; - - public Builder(IEnumerable values) - { - // TODO(cyrusn): Properly handle unicode normalization here. - var distinctValues = values.Where(v => v.Length > 0).Distinct(StringSliceComparer.OrdinalIgnoreCase).ToArray(); - var charCount = values.Sum(v => v.Length); - - _concatenatedLowerCaseWords = new char[charCount]; - _wordSpans = new TextSpan[distinctValues.Length]; - - var characterIndex = 0; - for (var i = 0; i < distinctValues.Length; i++) - { - var value = distinctValues[i]; - _wordSpans[i] = new TextSpan(characterIndex, value.Length); - - foreach (var ch in value) - { - _concatenatedLowerCaseWords[characterIndex] = CaseInsensitiveComparison.ToLower(ch); - characterIndex++; - } - } - - // We will have one node for each string value that we are adding. - _builderNodes = new BuilderNode[distinctValues.Length]; - _compactEdges = new Edge[distinctValues.Length * CompactEdgeAllocationSize]; - } - - internal BKTree Create() - { - for (var i = 0; i < _wordSpans.Length; i++) - { - Add(_wordSpans[i], insertionIndex: i); - } - - var nodes = ImmutableArray.CreateBuilder(_builderNodes.Length); - - // There will be one less edge in the graph than nodes. Each node (except for the - // root) will have a single edge pointing to it. - var edges = ImmutableArray.CreateBuilder(Math.Max(0, _builderNodes.Length - 1)); - - BuildArrays(nodes, edges); - - return new BKTree(_concatenatedLowerCaseWords, nodes.MoveToImmutable(), edges.MoveToImmutable()); - } - - private void BuildArrays(ImmutableArray.Builder nodes, ImmutableArray.Builder edges) - { - var currentEdgeIndex = 0; - for (var i = 0; i < _builderNodes.Length; i++) - { - var builderNode = _builderNodes[i]; - var edgeCount = builderNode.EdgeCount; - - nodes.Add(new Node(builderNode.CharacterSpan, edgeCount, currentEdgeIndex)); - - if (edgeCount > 0) - { - // First, copy any edges that are in the compact array. - var start = i * CompactEdgeAllocationSize; - var end = start + Math.Min(edgeCount, CompactEdgeAllocationSize); - for (var j = start; j < end; j++) - { - edges.Add(_compactEdges[j]); - } - - // Then, if we've spilled over any edges, copy them as well. - var spilledEdges = builderNode.SpilloverEdges; - if (spilledEdges != null) - { - Debug.Assert(spilledEdges.Count == (edgeCount - CompactEdgeAllocationSize)); - - foreach (var kvp in spilledEdges) - { - edges.Add(new Edge(kvp.Key, kvp.Value)); - } - } - } - - currentEdgeIndex += edgeCount; - } - - Debug.Assert(currentEdgeIndex == edges.Capacity); - Debug.Assert(currentEdgeIndex == edges.Count); - } - - private void Add(TextSpan characterSpan, int insertionIndex) - { - if (insertionIndex == 0) - { - _builderNodes[insertionIndex] = new BuilderNode(characterSpan); - return; - } - - var currentNodeIndex = 0; - while (true) - { - var currentNode = _builderNodes[currentNodeIndex]; - - // Determine the edit distance between these two words. Note: we do not use - // a threshold here as we need the actual edit distance so we can actually - // determine what edge to make or walk. - var editDistance = EditDistance.GetEditDistance( - _concatenatedLowerCaseWords.AsSpan(currentNode.CharacterSpan.Start, currentNode.CharacterSpan.Length), - _concatenatedLowerCaseWords.AsSpan(characterSpan.Start, characterSpan.Length)); - - if (editDistance == 0) - { - // This should never happen. We dedupe all items before proceeding to the 'Add' step. - // So the edit distance should always be non-zero. - throw new InvalidOperationException(); - } - - if (TryGetChildIndex(currentNode, currentNodeIndex, editDistance, out var childNodeIndex)) - { - // Edit distances collide. Move to this child and add this word to it. - currentNodeIndex = childNodeIndex; - continue; - } - - // found the node we want to add the child node to. - AddChildNode(characterSpan, insertionIndex, currentNode.EdgeCount, currentNodeIndex, editDistance); - return; - } - } - - private void AddChildNode( - TextSpan characterSpan, int insertionIndex, int currentNodeEdgeCount, int currentNodeIndex, int editDistance) - { - // The node as 'currentNodeIndex' doesn't have an edge with this edit distance. - // Three cases to handle: - // 1) there are less than 4 edges. We simply place the edge into the correct - // location in compactEdges - // 2) there are 4 edges. We need to make the spillover dictionary and then add - // the new edge into that. - // 3) there are more than 4 edges. Just put the new edge in the spillover - // dictionary. - - if (currentNodeEdgeCount < CompactEdgeAllocationSize) - { - _compactEdges[currentNodeIndex * CompactEdgeAllocationSize + currentNodeEdgeCount] = - new Edge(editDistance, insertionIndex); - } - else - { - // When we hit 4 elements, we need to allocate the spillover dictionary to - // place the extra edges. - if (currentNodeEdgeCount == CompactEdgeAllocationSize) - { - Debug.Assert(_builderNodes[currentNodeIndex].SpilloverEdges == null); - var spilloverEdges = new Dictionary(); - _builderNodes[currentNodeIndex].SpilloverEdges = spilloverEdges; - } - - _builderNodes[currentNodeIndex].SpilloverEdges.Add(editDistance, insertionIndex); - } - - _builderNodes[currentNodeIndex].EdgeCount++; - _builderNodes[insertionIndex] = new BuilderNode(characterSpan); - return; - } - - private bool TryGetChildIndex(BuilderNode currentNode, int currentNodeIndex, int editDistance, out int childIndex) - { - // linearly scan the children we have to see if there is one with this edit distance. - var start = currentNodeIndex * CompactEdgeAllocationSize; - var end = start + Math.Min(currentNode.EdgeCount, CompactEdgeAllocationSize); - - for (var i = start; i < end; i++) - { - if (_compactEdges[i].EditDistance == editDistance) - { - childIndex = _compactEdges[i].ChildNodeIndex; - return true; - } - } - - // If we've spilled over any edges, check there as well - if (currentNode.SpilloverEdges != null) - { - // Can't use the compact array. Have to use the spillover dictionary instead. - Debug.Assert(currentNode.SpilloverEdges.Count == (currentNode.EdgeCount - CompactEdgeAllocationSize)); - return currentNode.SpilloverEdges.TryGetValue(editDistance, out childIndex); - } - - childIndex = -1; - return false; - } - - private struct BuilderNode - { - public readonly TextSpan CharacterSpan; - public int EdgeCount; - public Dictionary SpilloverEdges; - - public BuilderNode(TextSpan characterSpan) : this() - { - this.CharacterSpan = characterSpan; - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Edge.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Edge.cs deleted file mode 100644 index cc276148..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Edge.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Roslyn.Utilities -{ - internal partial class BKTree - { - private struct Edge - { - // The edit distance between the child and parent connected by this edge. - // The child can be found in _nodes at ChildNodeIndex. - public readonly int EditDistance; - - /// Where the child node can be found in . - public readonly int ChildNodeIndex; - - public Edge(int editDistance, int childNodeIndex) - { - EditDistance = editDistance; - ChildNodeIndex = childNodeIndex; - } - - internal void WriteTo(ObjectWriter writer) - { - writer.WriteInt32(EditDistance); - writer.WriteInt32(ChildNodeIndex); - } - - internal static Edge ReadFrom(ObjectReader reader) - { - return new Edge(editDistance: reader.ReadInt32(), childNodeIndex: reader.ReadInt32()); - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Node.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Node.cs deleted file mode 100644 index 39677d6a..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Node.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Text; - -namespace Roslyn.Utilities -{ - internal partial class BKTree - { - private struct Node - { - /// - /// The string this node corresponds to. Specifically, this span is the range of - /// for that string. - /// - public readonly TextSpan WordSpan; - - ///How many child edges this node has. - public readonly int EdgeCount; - - ///Where the first edge can be found in . The edges - ///are in the range _edges[FirstEdgeIndex, FirstEdgeIndex + EdgeCount) - /// - public readonly int FirstEdgeIndex; - - public Node(TextSpan wordSpan, int edgeCount, int firstEdgeIndex) - { - WordSpan = wordSpan; - EdgeCount = edgeCount; - FirstEdgeIndex = firstEdgeIndex; - } - - internal void WriteTo(ObjectWriter writer) - { - writer.WriteInt32(WordSpan.Start); - writer.WriteInt32(WordSpan.Length); - writer.WriteInt32(EdgeCount); - writer.WriteInt32(FirstEdgeIndex); - } - - internal static Node ReadFrom(ObjectReader reader) - { - return new Node( - new TextSpan(start: reader.ReadInt32(), length: reader.ReadInt32()), - edgeCount: reader.ReadInt32(), firstEdgeIndex: reader.ReadInt32()); - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Serialization.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Serialization.cs deleted file mode 100644 index 2a900c2b..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.Serialization.cs +++ /dev/null @@ -1,68 +0,0 @@ -// 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; -using Microsoft.Extensions.Logging; - -namespace Roslyn.Utilities -{ - internal partial class BKTree - { - internal void WriteTo(ObjectWriter writer) - { - writer.WriteInt32(_concatenatedLowerCaseWords.Length); - foreach (var c in _concatenatedLowerCaseWords) - { - writer.WriteChar(c); - } - - writer.WriteInt32(_nodes.Length); - foreach (var node in _nodes) - { - node.WriteTo(writer); - } - - writer.WriteInt32(_edges.Length); - foreach (var edge in _edges) - { - edge.WriteTo(writer); - } - } - - internal static BKTree ReadFrom(ObjectReader reader, ILogger logger) - { - try - { - var concatenatedLowerCaseWords = new char[reader.ReadInt32()]; - for (var i = 0; i < concatenatedLowerCaseWords.Length; i++) - { - concatenatedLowerCaseWords[i] = reader.ReadChar(); - } - - var nodeCount = reader.ReadInt32(); - var nodes = ImmutableArray.CreateBuilder(nodeCount); - for (var i = 0; i < nodeCount; i++) - { - nodes.Add(Node.ReadFrom(reader)); - } - - var edgeCount = reader.ReadInt32(); - var edges = ImmutableArray.CreateBuilder(edgeCount); - for (var i = 0; i < edgeCount; i++) - { - edges.Add(Edge.ReadFrom(reader)); - } - - return new BKTree(concatenatedLowerCaseWords, nodes.MoveToImmutable(), edges.MoveToImmutable()); - } - catch (Exception ex) - { - LogExceptionInCacheRead (logger, ex); - return null; - } - } - - [LoggerMessage (EventId = 0, Level = LogLevel.Error, Message = "Exception in BKTree cache read")] - static partial void LogExceptionInCacheRead (ILogger logger, Exception ex); - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.cs deleted file mode 100644 index eb785f5f..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BKTree.cs +++ /dev/null @@ -1,216 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Utilities; -using System; - -namespace Roslyn.Utilities -{ - /// - /// NOTE: Only use if you truly need a BK-tree. If you just want to compare words, use - /// the type instead. - /// - /// An implementation of a Burkhard-Keller tree. Introduced in: - /// - /// 'Some approaches to best-match file searching.' - /// Communications of the ACM CACM - /// Volume 16 Issue 4, April 1973 - /// Pages 230-236 - /// http://dl.acm.org/citation.cfm?doid=362003.362025 - /// - internal partial class BKTree - { - public static readonly BKTree Empty = new BKTree( - Array.Empty(), - ImmutableArray.Empty, - ImmutableArray.Empty); - - // We have three completely flat arrays of structs. These arrays fully represent the - // BK tree. The structure is as follows: - // - // The root node is in _nodes[0]. - // - // It lists the count of edges it has. These edges are in _edges in the range - // [0*, childCount). Each edge has the index of the child node it points to, and the - // edit distance between the parent and the child. - // - // * of course '0' is only for the root case. - // - // All nodes state where in _edges their child edges range starts, so the children - // for any node are in the range[node.FirstEdgeIndex, node.FirstEdgeIndex + node.EdgeCount). - // - // Each node also has an associated string. These strings are concatenated and stored - // in _concatenatedLowerCaseWords. Each node has a TextSpan that indicates which portion - // of the character array is their string. Note: i'd like to use an immutable array - // for the characters as well. However, we need to create slices, and they need to - // work on top of an ArraySlice (which needs a char[]). The edit distance code also - // wants to work on top of raw char[]s (both for speed, and so it can pool arrays - // to prevent lots of garbage). Because of that we just keep this as a char[]. - private readonly char[] _concatenatedLowerCaseWords; - private readonly ImmutableArray _nodes; - private readonly ImmutableArray _edges; - - private BKTree(char[] concatenatedLowerCaseWords, ImmutableArray nodes, ImmutableArray edges) - { - _concatenatedLowerCaseWords = concatenatedLowerCaseWords; - _nodes = nodes; - _edges = edges; - } - - public static BKTree Create(params string[] values) - { - return Create(values.Select(v => new StringSlice(v))); - } - - public static BKTree Create(IEnumerable values) - { - return new Builder(values).Create(); - } - - public IList Find(string value, int? threshold = null) - { - if (_nodes.Length == 0) - { - return SpecializedCollections.EmptyList(); - } - - var lowerCaseCharacters = ArrayPool.GetArray(value.Length); - try - { - for (var i = 0; i < value.Length; i++) - { - lowerCaseCharacters[i] = CaseInsensitiveComparison.ToLower(value[i]); - } - - threshold ??= WordSimilarityChecker.GetThreshold(value); - var result = new List(); - Lookup(_nodes[0], lowerCaseCharacters, value.Length, threshold.Value, result, recursionCount: 0); - return result; - } - finally - { - ArrayPool.ReleaseArray(lowerCaseCharacters); - } - } - - private void Lookup( - Node currentNode, - char[] queryCharacters, - int queryLength, - int threshold, - List result, - int recursionCount) - { - // Don't bother recursing too deeply in the case of pathological trees. - // This really only happens when the actual code is strange (like - // 10,000 symbols all a single letter long). In htat case, searching - // down this path will be fairly fruitless anyways. - // - // Note: this won't affect good searches against good data even if this - // pathological chain exists. That's because the good items will still - // cluster near the root node in the tree, and won't be off the end of - // this long chain. - if (recursionCount > 256) - { - return; - } - - // We always want to compute the real edit distance (ignoring any thresholds). This is - // because we need that edit distance to appropriately determine which edges to walk - // in the tree. - var characterSpan = currentNode.WordSpan; - var editDistance = EditDistance.GetEditDistance( - _concatenatedLowerCaseWords.AsSpan(characterSpan.Start, characterSpan.Length), - queryCharacters.AsSpan(0, queryLength)); - - if (editDistance <= threshold) - { - // Found a match. - result.Add(new string(_concatenatedLowerCaseWords, characterSpan.Start, characterSpan.Length)); - } - - var min = editDistance - threshold; - var max = editDistance + threshold; - - var startInclusive = currentNode.FirstEdgeIndex; - var endExclusive = startInclusive + currentNode.EdgeCount; - for (var i = startInclusive; i < endExclusive; i++) - { - var childEditDistance = _edges[i].EditDistance; - if (min <= childEditDistance && childEditDistance <= max) - { - Lookup(_nodes[_edges[i].ChildNodeIndex], - queryCharacters, queryLength, threshold, result, - recursionCount + 1); - } - } - } - -#if false - // Used for diagnostic purposes. - internal void DumpStats() - { - var sb = new StringBuilder(); - sb.AppendLine("Nodes length: " + _nodes.Length); - var childCountHistogram = new Dictionary(); - - foreach (var node in _nodes) - { - var childCount = node.EdgeCount; - int existing; - childCountHistogram.TryGetValue(childCount, out existing); - - childCountHistogram[childCount] = existing + 1; - } - - sb.AppendLine(); - sb.AppendLine("Child counts:"); - foreach (var kvp in childCountHistogram.OrderBy(kvp => kvp.Key)) - { - sb.AppendLine(kvp.Key + "\t" + kvp.Value); - } - - // An item is dense if, starting from 1, at least 80% of it's array would be full. - var densities = new int[11]; - var empyCount = 0; - - foreach (var node in _nodes) - { - if (node.EdgeCount == 0) - { - empyCount++; - continue; - } - - var maxEditDistance = -1; - var startInclusive = node.FirstEdgeIndex; - var endExclusive = startInclusive + node.EdgeCount; - for (var i = startInclusive; i < endExclusive; i++) - { - maxEditDistance = Max(maxEditDistance, _edges[i].EditDistance); - } - - var editDistanceCount = node.EdgeCount; - - var bucket = 10 * editDistanceCount / maxEditDistance; - densities[bucket]++; - } - - var nonEmptyCount = _nodes.Length - empyCount; - sb.AppendLine(); - sb.AppendLine("NoChildren: " + empyCount); - sb.AppendLine("AnyChildren: " + nonEmptyCount); - sb.AppendLine("Densities:"); - for (var i = 0; i < densities.Length; i++) - { - sb.AppendLine("<=" + i + "0% = " + densities[i] + ", " + ((float)densities[i] / nonEmptyCount)); - } - - var result = sb.ToString(); - } -#endif - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BitVector.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BitVector.cs deleted file mode 100644 index f86d1f4d..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/BitVector.cs +++ /dev/null @@ -1,375 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using Roslyn.Utilities; -using Word = System.UInt64; - -namespace Microsoft.CodeAnalysis -{ - [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] - internal struct BitVector : IEquatable - { - private const Word ZeroWord = 0; - private const int Log2BitsPerWord = 6; - - public const int BitsPerWord = 1 << Log2BitsPerWord; - - // Cannot expose the following two field publicly because this structure is mutable - // and might become not null/empty, unless we restrict access to it. - private static readonly Word[] s_emptyArray = Array.Empty(); - private static readonly BitVector s_nullValue = default; - private static readonly BitVector s_emptyValue = new BitVector(0, s_emptyArray, 0); - - private Word _bits0; - private Word[] _bits; - private int _capacity; - - private BitVector(Word bits0, Word[] bits, int capacity) - { - int requiredWords = WordsForCapacity(capacity); - Debug.Assert(requiredWords == 0 || requiredWords <= bits.Length); - _bits0 = bits0; - _bits = bits; - _capacity = capacity; - Check(); - } - - public bool Equals(BitVector other) - { - // Bit arrays only equal if their underlying sets are of the same size - return _capacity == other._capacity - // and have the same set of bits set - && _bits0 == other._bits0 - && _bits.AsSpan().SequenceEqual(other._bits.AsSpan()); - } - - public override bool Equals(object obj) - { - return obj is BitVector && Equals((BitVector)obj); - } - - public static bool operator ==(BitVector left, BitVector right) - { - return left.Equals(right); - } - - public static bool operator !=(BitVector left, BitVector right) - { - return !left.Equals(right); - } - - public override int GetHashCode() - { - int bitsHash = _bits0.GetHashCode(); - - if (_bits != null) - { - for (int i = 0; i < _bits.Length; i++) - { - bitsHash = Hash.Combine(_bits[i].GetHashCode(), bitsHash); - } - } - - return Hash.Combine(_capacity, bitsHash); - } - - private static int WordsForCapacity(int capacity) - { - if (capacity <= 0) return 0; - int lastIndex = (capacity - 1) >> Log2BitsPerWord; - return lastIndex; - } - - public int Capacity => _capacity; - - [Conditional("DEBUG_BITARRAY")] - private void Check() - { - Debug.Assert(_capacity == 0 || WordsForCapacity(_capacity) <= _bits.Length); - } - - public void EnsureCapacity(int newCapacity) - { - if (newCapacity > _capacity) - { - int requiredWords = WordsForCapacity(newCapacity); - if (requiredWords > _bits.Length) Array.Resize(ref _bits, requiredWords); - _capacity = newCapacity; - Check(); - } - Check(); - } - - public IEnumerable Words() - { - if (_capacity > 0) - { - yield return _bits0; - } - - for (int i = 0, n = _bits?.Length ?? 0; i < n; i++) - { - yield return _bits[i]; - } - } - - public IEnumerable TrueBits() - { - Check(); - if (_bits0 != 0) - { - for (int bit = 0; bit < BitsPerWord; bit++) - { - Word mask = ((Word)1) << bit; - if ((_bits0 & mask) != 0) - { - if (bit >= _capacity) yield break; - yield return bit; - } - } - } - for (int i = 0; i < _bits.Length; i++) - { - Word w = _bits[i]; - if (w != 0) - { - for (int b = 0; b < BitsPerWord; b++) - { - Word mask = ((Word)1) << b; - if ((w & mask) != 0) - { - int bit = ((i + 1) << Log2BitsPerWord) | b; - if (bit >= _capacity) yield break; - yield return bit; - } - } - } - } - } - - /// - /// Create BitArray with at least the specified number of bits. - /// - public static BitVector Create(int capacity) - { - int requiredWords = WordsForCapacity(capacity); - Word[] bits = (requiredWords == 0) ? s_emptyArray : new Word[requiredWords]; - return new BitVector(0, bits, capacity); - } - - /// - /// return a bit array with all bits set from index 0 through bitCount-1 - /// - /// - /// - public static BitVector AllSet(int capacity) - { - if (capacity == 0) - { - return Empty; - } - - int requiredWords = WordsForCapacity(capacity); - Word[] bits = (requiredWords == 0) ? s_emptyArray : new Word[requiredWords]; - int lastWord = requiredWords - 1; - Word bits0 = ~ZeroWord; - for (int j = 0; j < lastWord; j++) - bits[j] = ~ZeroWord; - int numTrailingBits = capacity & ((BitsPerWord) - 1); - if (numTrailingBits > 0) - { - Debug.Assert(numTrailingBits <= BitsPerWord); - Word lastBits = ~((~ZeroWord) << numTrailingBits); - if (lastWord < 0) - { - bits0 = lastBits; - } - else - { - bits[lastWord] = lastBits; - } - } - else if (requiredWords > 0) - { - bits[lastWord] = ~ZeroWord; - } - - return new BitVector(bits0, bits, capacity); - } - - /// - /// Make a copy of a bit array. - /// - /// - public BitVector Clone() - { - return new BitVector(_bits0, (_bits == null) ? null : (_bits.Length == 0) ? s_emptyArray : (Word[])_bits.Clone(), _capacity); - } - - /// - /// Is the given bit array null? - /// - public bool IsNull - { - get - { - return _bits == null; - } - } - - public static BitVector Null => s_nullValue; - - public static BitVector Empty => s_emptyValue; - - /// - /// Modify this bit vector by bitwise AND-ing each element with the other bit vector. - /// For the purposes of the intersection, any bits beyond the current length will be treated as zeroes. - /// Return true if any changes were made to the bits of this bit vector. - /// - public bool IntersectWith(in BitVector other) - { - bool anyChanged = false; - int otherLength = other._bits.Length; - var thisBits = _bits; - int thisLength = thisBits.Length; - - if (otherLength > thisLength) - otherLength = thisLength; - - // intersect the inline portion - { - var oldV = _bits0; - var newV = oldV & other._bits0; - if (newV != oldV) - { - _bits0 = newV; - anyChanged = true; - } - } - // intersect up to their common length. - for (int i = 0; i < otherLength; i++) - { - var oldV = thisBits[i]; - var newV = oldV & other._bits[i]; - if (newV != oldV) - { - thisBits[i] = newV; - anyChanged = true; - } - } - - // treat the other bit array as being extended with zeroes - for (int i = otherLength; i < thisLength; i++) - { - if (thisBits[i] != 0) - { - thisBits[i] = 0; - anyChanged = true; - } - } - - Check(); - return anyChanged; - } - - /// - /// Modify this bit vector by '|'ing each element with the other bit vector. - /// - /// - /// True if any bits were set as a result of the union. - /// - public bool UnionWith(in BitVector other) - { - bool anyChanged = false; - - if (other._capacity > _capacity) - EnsureCapacity(other._capacity); - - Word oldbits = _bits0; - _bits0 |= other._bits0; - - if (oldbits != _bits0) - anyChanged = true; - - for (int i = 0; i < other._bits.Length; i++) - { - oldbits = _bits[i]; - _bits[i] |= other._bits[i]; - - if (_bits[i] != oldbits) - anyChanged = true; - } - - Check(); - - return anyChanged; - } - - public bool this[int index] - { - get - { - if (index >= _capacity) - return false; - int i = (index >> Log2BitsPerWord) - 1; - var word = (i < 0) ? _bits0 : _bits[i]; - - return IsTrue(word, index); - } - - set - { - if (index >= _capacity) - EnsureCapacity(index + 1); - int i = (index >> Log2BitsPerWord) - 1; - int b = index & (BitsPerWord - 1); - Word mask = ((Word)1) << b; - if (i < 0) - { - if (value) - _bits0 |= mask; - else - _bits0 &= ~mask; - } - else - { - if (value) - _bits[i] |= mask; - else - _bits[i] &= ~mask; - } - } - } - - public void Clear() - { - _bits0 = 0; - if (_bits != null) Array.Clear(_bits, 0, _bits.Length); - } - - public static bool IsTrue(Word word, int index) - { - int b = index & (BitsPerWord - 1); - Word mask = ((Word)1) << b; - return (word & mask) != 0; - } - - public static int WordsRequired(int capacity) - { - if (capacity <= 0) return 0; - return WordsForCapacity(capacity) + 1; - } - - internal string GetDebuggerDisplay() - { - var value = new char[_capacity]; - for (int i = 0; i < _capacity; i++) - { - value[_capacity - i - 1] = this[i] ? '1' : '0'; - } - return new string(value); - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Checksum.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Checksum.cs deleted file mode 100644 index 7bf1177d..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Checksum.cs +++ /dev/null @@ -1,220 +0,0 @@ -// 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.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Runtime.InteropServices; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis -{ - /// - /// Checksum of data can be used later to see whether two data are same or not - /// without actually comparing data itself - /// - internal sealed partial class Checksum : IObjectWritable, IEquatable - { - /// - /// The intended size of the structure. - /// - private const int HashSize = 20; - - public static readonly Checksum Null = new Checksum(default); - - private readonly HashData _checksum; - - /// - /// Create Checksum from given byte array. if byte array is bigger than - /// , it will be truncated to the size - /// - public static Checksum From(byte[] checksum) - { - if (checksum.Length == 0) - { - return Null; - } - - if (checksum.Length < HashSize) - { - throw new ArgumentException($"checksum must be equal or bigger than the hash size: {HashSize}", nameof(checksum)); - } - - return FromWorker(checksum); - } - - /// - /// Create Checksum from given byte array. if byte array is bigger than - /// , it will be truncated to the size - /// - public static Checksum From(ImmutableArray checksum) - { - if (checksum.Length == 0) - { - return Null; - } - - if (checksum.Length < HashSize) - { - throw new ArgumentException($"{nameof(checksum)} must be equal or bigger than the hash size: {HashSize}", nameof(checksum)); - } - - using var pooled = SharedPools.ByteArray.GetPooledObject(); - var bytes = pooled.Object; - checksum.CopyTo(sourceIndex: 0, bytes, destinationIndex: 0, length: HashSize); - - return FromWorker(bytes); - } - - public static Checksum FromSerialized(byte[] checksum) - { - if (checksum.Length == 0) - { - return Null; - } - - if (checksum.Length != HashSize) - { - throw new ArgumentException($"{nameof(checksum)} must be equal to the hash size: {HashSize}", nameof(checksum)); - } - - return FromWorker(checksum); - } - - private static unsafe Checksum FromWorker(byte[] checksum) - { - fixed (byte* data = checksum) - { - // Avoid a direct dereferencing assignment since sizeof(HashData) may be greater than HashSize. - // - // ex) "https://bugzilla.xamarin.com/show_bug.cgi?id=60298" - LayoutKind.Explicit, Size = 12 ignored with 64bit alignment - // or "https://github.com/dotnet/roslyn/issues/23722" - Checksum throws on Mono 64-bit - return new Checksum(HashData.FromPointer((HashData*)data)); - } - } - - private Checksum(HashData hash) - { - _checksum = hash; - } - - public bool Equals(Checksum other) - { - if (other == null) - { - return false; - } - - return _checksum == other._checksum; - } - - public override bool Equals(object obj) - => Equals(obj as Checksum); - - public override int GetHashCode() - => _checksum.GetHashCode(); - - public override unsafe string ToString() - { - var data = new byte[sizeof(HashData)]; - fixed (byte* dataPtr = data) - { - *(HashData*)dataPtr = _checksum; - } - - return Convert.ToBase64String(data, 0, HashSize); - } - - public static bool operator ==(Checksum left, Checksum right) - { - return EqualityComparer.Default.Equals(left, right); - } - - public static bool operator !=(Checksum left, Checksum right) - { - return !(left == right); - } - - bool IObjectWritable.ShouldReuseInSerialization => true; - - public void WriteTo(ObjectWriter writer) - => _checksum.WriteTo(writer); - - public static Checksum ReadFrom(ObjectReader reader) - => new Checksum(HashData.ReadFrom(reader)); - - public static string GetChecksumLogInfo(Checksum checksum) - { - return checksum.ToString(); - } - - public static string GetChecksumsLogInfo(IEnumerable checksums) - { - return string.Join("|", checksums.Select(c => c.ToString())); - } - - /// - /// This structure stores the 20-byte hash as an inline value rather than requiring the use of - /// byte[]. - /// - [StructLayout(LayoutKind.Explicit, Size = HashSize)] - private struct HashData : IEquatable - { - [FieldOffset(0)] - private long Data1; - - [FieldOffset(8)] - private long Data2; - - [FieldOffset(16)] - private int Data3; - - public static bool operator ==(HashData x, HashData y) - => x.Equals(y); - - public static bool operator !=(HashData x, HashData y) - => !x.Equals(y); - - public void WriteTo(ObjectWriter writer) - { - writer.WriteInt64(Data1); - writer.WriteInt64(Data2); - writer.WriteInt32(Data3); - } - - public static unsafe HashData FromPointer(HashData* hash) - { - HashData result = default; - result.Data1 = hash->Data1; - result.Data2 = hash->Data2; - result.Data3 = hash->Data3; - return result; - } - - public static HashData ReadFrom(ObjectReader reader) - { - HashData result = default; - result.Data1 = reader.ReadInt64(); - result.Data2 = reader.ReadInt64(); - result.Data3 = reader.ReadInt32(); - return result; - } - - public override int GetHashCode() - { - // The checksum is already a hash. Just read a 4-byte value to get a well-distributed hash code. - return (int)Data1; - } - - public override bool Equals(object obj) - => obj is HashData other && Equals(other); - - public bool Equals(HashData other) - { - return Data1 == other.Data1 - && Data2 == other.Data2 - && Data3 == other.Data3; - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/EditDistance.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/EditDistance.cs deleted file mode 100644 index 8bc530e6..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/EditDistance.cs +++ /dev/null @@ -1,645 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using System.Text; -using System.Threading; -using Microsoft.CodeAnalysis; - -namespace Roslyn.Utilities -{ - /// - /// NOTE: Only use if you truly need an edit distance. If you just want to compare words, use - /// the type instead. - /// - /// Implementation of the Damerau-Levenshtein edit distance algorithm from: - /// An Extension of the String-to-String Correction Problem: - /// Published in Journal of the ACM (JACM) - /// Volume 22 Issue 2, April 1975. - /// - /// Important, unlike many edit distance algorithms out there, this one implements a true metric - /// that satisfies the triangle inequality. (Unlike the "Optimal String Alignment" or "Restricted - /// string edit distance" solutions which do not). This means this edit distance can be used in - /// other domains that require the triangle inequality (like BKTrees). - /// - /// Specifically, this implementation satisfies the following inequality: D(x, y) + D(y, z) >= D(x, z) - /// (where D is the edit distance). - /// - internal class EditDistance : IDisposable - { - // Our edit distance algorithm makes use of an 'infinite' value. A value so high that it - // could never participate in an edit distance (and effectively means the path through it - // is dead). - // - // We do *not* represent this with "int.MaxValue" due to the presence of certain addition - // operations in the edit distance algorithm. These additions could cause int.MaxValue - // to roll over to a very negative value (which would then look like the lowest cost - // path). - // - // So we pick a value that is both effectively larger than any possible edit distance, - // and also has no chance of overflowing. - private const int Infinity = int.MaxValue >> 1; - - public const int BeyondThreshold = int.MaxValue; - - private string _source; - private char[] _sourceLowerCaseCharacters; - - public EditDistance (string text) - { - _source = text ?? throw new ArgumentNullException (nameof (text)); - _sourceLowerCaseCharacters = ConvertToLowercaseArray (text); - } - - private static char[] ConvertToLowercaseArray (string text) - { - var array = ArrayPool.GetArray (text.Length); - for (var i = 0; i < text.Length; i++) { - array[i] = CaseInsensitiveComparison.ToLower (text[i]); - } - - return array; - } - - public void Dispose () - { - ArrayPool.ReleaseArray (_sourceLowerCaseCharacters); - _source = null; - _sourceLowerCaseCharacters = null; - } - - public static int GetEditDistance (string source, string target, int threshold = int.MaxValue) - { - using var editDistance = new EditDistance (source); - return editDistance.GetEditDistance (target, threshold); - } - - public static int GetEditDistance (char[] source, char[] target, int threshold = int.MaxValue) - { - return GetEditDistance (source.AsSpan (), target.AsSpan (), threshold); - } - - public int GetEditDistance (string target, int threshold = int.MaxValue) - { - if (_sourceLowerCaseCharacters == null) { - throw new ObjectDisposedException (nameof (EditDistance)); - } - - var targetLowerCaseCharacters = ConvertToLowercaseArray (target); - try { - return GetEditDistance ( - _sourceLowerCaseCharacters.AsSpan (0, _source.Length), - targetLowerCaseCharacters.AsSpan (0, target.Length), - threshold); - } finally { - ArrayPool.ReleaseArray (targetLowerCaseCharacters); - } - } - - private const int MaxMatrixPoolDimension = 64; - private static readonly ThreadLocal t_matrixPool = - new ThreadLocal (() => InitializeMatrix (new int[MaxMatrixPoolDimension, MaxMatrixPoolDimension])); - - // To find swapped characters we make use of a table that keeps track of the last location - // we found that character. For performance reasons we only do this work for ascii characters - // (i.e. with value <= 127). This allows us to just use a simple array we can index into instead - // of needing something more expensive like a dictionary. - private const int LastSeenIndexLength = 128; - private static ThreadLocal t_lastSeenIndexPool = - new ThreadLocal (() => new int[LastSeenIndexLength]); - - private static int[,] GetMatrix (int width, int height) - { - if (width > MaxMatrixPoolDimension || height > MaxMatrixPoolDimension) { - return InitializeMatrix (new int[width, height]); - } - - return t_matrixPool.Value; - } - - private static int[,] InitializeMatrix (int[,] matrix) - { - // All matrices share the following in common: - // - // ------------------ - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 7 - // |∞ 1 - // |∞ 2 - // |∞ 3 - // |∞ 4 - // |∞ 5 - // |∞ 6 - // |∞ 7 - // - // So we initialize this once when the matrix is created. For pooled arrays we only - // have to do this once, and it will retain this layout for all future computations. - - var width = matrix.GetLength (0); - var height = matrix.GetLength (1); - - for (var i = 0; i < width; i++) { - matrix[i, 0] = Infinity; - - if (i < width - 1) { - matrix[i + 1, 1] = i; - } - } - - for (var j = 0; j < height; j++) { - matrix[0, j] = Infinity; - - if (j < height - 1) { - matrix[1, j + 1] = j; - } - } - - return matrix; - } - - public static int GetEditDistance (ReadOnlySpan source, ReadOnlySpan target, int threshold = int.MaxValue) - { - return source.Length <= target.Length - ? GetEditDistanceWorker (source, target, threshold) - : GetEditDistanceWorker (target, source, threshold); - } - - private static int GetEditDistanceWorker (ReadOnlySpan source, ReadOnlySpan target, int threshold) - { - // Note: sourceLength will always be smaller or equal to targetLength. - // - // Also Note: sourceLength and targetLength values will mutate and represent the lengths - // of the portions of the arrays we want to compare. However, even after mutation, hte - // invariant that sourceLength is <= targetLength will remain. - Debug.Assert (source.Length <= target.Length); - - // First: - // Determine the common prefix/suffix portions of the strings. We don't even need to - // consider them as they won't add anything to the edit cost. - while (source.Length > 0 && source[source.Length - 1] == target[target.Length - 1]) { - source = source.Slice (0, source.Length - 1); - target = target.Slice (0, target.Length - 1); - } - - while (source.Length > 0 && source[0] == target[0]) { - source = source.Slice (1); - target = target.Slice (1); - } - - // 'sourceLength' and 'targetLength' are now the lengths of the substrings of our strings that we - // want to compare. 'startIndex' is the starting point of the substrings in both array. - // - // If we've matched all of the 'source' string in the prefix and suffix of 'target'. then the edit - // distance is just whatever operations we have to create the remaining target substring. - // - // Note: we don't have to check if targetLength is 0. That's because targetLength being zero would - // necessarily mean that sourceLength is 0. - var sourceLength = source.Length; - var targetLength = target.Length; - if (sourceLength == 0) { - return targetLength <= threshold ? targetLength : BeyondThreshold; - } - - // The is the minimum number of edits we'd have to make. i.e. if 'source' and - // 'target' are the same length, then we might not need to make any edits. However, - // if target has length 10 and source has length 7, then we're going to have to - // make at least 3 edits no matter what. - var minimumEditCount = targetLength - sourceLength; - Debug.Assert (minimumEditCount >= 0); - - // If the number of edits we'd have to perform is greater than our threshold, then - // there's no point in even continuing. - if (minimumEditCount > threshold) { - return BeyondThreshold; - } - - // Say we want to find the edit distance between "sunday" and "saturday". Our initial - // matrix will be: - // - // (Note: for purposes of this explanation we will not be trimming off the common - // prefix/suffix of the strings. That optimization does not affect any of the - // remainder of the explanation). - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 - // a |∞ 2 - // t |∞ 3 - // u |∞ 4 - // r |∞ 5 - // d |∞ 6 - // a |∞ 7 - // y |∞ 8 - // - // Note that the matrix will always be square, or a rectangle that is taller htan it is - // longer. Our 'source' is at the top, and our 'target' is on the left. The edit distance - // between any prefix of 'source' and any prefix of 'target' can then be found in - // the unfilled area of the matrix. Specifically, if we have source.substring(0, m) and - // target.substring(0, n), then the edit distance for them can be found at matrix position - // (m+1, n+1). This is why the 1'th row and 1'th column can be prefilled. They represent - // the cost to go from the empty target to the full source or the empty source to the full - // target (respectively). So, if we wanted to know the edit distance between "sun" and - // "sat", we'd look at (3+1, 3+1). It then follows that our final edit distance between - // the full source and target is in the lower right corner of this matrix. - // - // If we fill out the matrix fully we'll get: - // - // s u n d a y <-- source - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 0 1 2 3 4 5 - // a |∞ 2 1 1 2 3 3 4 - // t |∞ 3 2 2 2 3 4 4 - // u |∞ 4 3 2 3 3 4 5 - // r |∞ 5 4 3 3 4 4 5 - // d |∞ 6 5 4 4 3 4 5 - // a |∞ 7 6 5 5 4 3 4 - // y |∞ 8 7 6 6 5 4 3 <-- - // ^ - // | - // - // So in this case, the edit distance is 3. Or, specifically, the edits: - // - // Sunday -> Replace("n", "r") -> - // Surday -> Insert("a") -> - // Saurday -> Insert("t") -> - // Saturday - // - // - // Now: in the case where we want to know what the edit distance actually is (for example - // when making a BKTree), we must fill out this entire array to get the true edit distance. - // - // However, in some cases we can do a bit better. For example, if a client only wants to - // the edit distance *when the edit distance will be less than some threshold* then we do - // not need to examine the entire matrix. We only want to examine until the point where - // we realize that, no matter what, our final edit distance will be more than that threshold - // (at which point we can return early). - // - // Some things are trivially easy to check. First, the edit distance between two strings is at - // *best* the difference of their lengths. i.e. if i have "aa" and "aaaaa" then the edit - // distance is 3 (the difference of 5 and 2). If our threshold is less then 3 then there - // is no way these two strings could match. So we can leave early if we can tell it would - // simply be impossible to get an edit distance within the specified threshold. - // - // Second, let's look at our matrix again: - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 - // a |∞ 2 - // t |∞ 3 - // u |∞ 4 - // r |∞ 5 - // d |∞ 6 - // a |∞ 7 - // y |∞ 8 * - // - // We want to know what the value is at *, and we want to stop as early as possible if it - // is greater than our threshold. - // - // Given the edit distance rules we observe edit distance at any point (i,j) in the matrix will - // always be greater than or equal to the value in (i-1, j-1). i.e. the edit distance of - // any two strings is going to be *at best* equal to the edit distance of those two strings - // without their final characters. If their final characters are the same, they'll have the - // same edit distance. If they are different, the edit distance will be greater. Given - // that we know the final edit distance is in the lower right, we can discover something - // useful in the matrix. - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 - // a |∞ 2 - // t |∞ 3 ` - // u |∞ 4 ` - // r |∞ 5 ` - // d |∞ 6 ` - // a |∞ 7 ` - // y |∞ 8 * - // - // The slashes are the "bottom" diagonal leading to the lower right. The value in the - // lower right will be strictly equal to or greater than any value on this diagonal. - // Thus, if that value exceeds the threshold, we know we can stop immediately as the - // total edit distance must be greater than the threshold. - // - // We can use similar logic to avoid even having to examine more of the matrix when we - // have a threshold. First, consider the same diagonal. - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 - // a |∞ 2 - // t |∞ 3 ` - // u |∞ 4 ` x - // r |∞ 5 ` | - // d |∞ 6 ` | - // a |∞ 7 ` | - // y |∞ 8 * - // - // And then consider a point above that diagonal (indicated by x). In the example - // above, the edit distance to * from 'x' will be (x+4). If, for example, threshold - // was '2', then it would be impossible for the path from 'x' to provide a good - // enough edit distance *ever*. Similarly: - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 - // a |∞ 2 - // t |∞ 3 ` - // u |∞ 4 ` - // r |∞ 5 ` - // d |∞ 6 ` - // a |∞ 7 ` - // y |∞ 8 y - - * - // - // Here we see that the final edit distance will be "y+3". Again, if the edit - // distance threshold is less than 3, then no path from y will provide a good - // enough edit distance. - // - // So, if we had an edit distance threshold of 3, then the range around that - // bottom diagonal that we should consider checking is: - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 | | - // a |∞ 2 | | | - // t |∞ 3 ` | | | - // u |∞ 4 - ` | | | - // r |∞ 5 - - ` | | | - // d |∞ 6 - - - ` | | - // a |∞ 7 - - - ` | - // y |∞ 8 - - - * - // - // Now, also consider that it will take a minimum of targetLength-sourceLength edits - // just to move to the lower diagonal from the upper diagonal. That leaves - // 'threshold - (targetLength - sourceLength)' edits remaining. In this example, that - // means '3 - (8 - 6)' = 1. Because of this our lower diagonal offset is capped at: - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 | | - // a |∞ 2 | | | - // t |∞ 3 ` | | | - // u |∞ 4 - ` | | | - // r |∞ 5 - ` | | | - // d |∞ 6 - ` | | - // a |∞ 7 - ` | - // y |∞ 8 - * - // - // If we mark the upper diagonal appropriately we see the matrix as: - // - // s u n d a y - // ---------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 - // s |∞ 1 ` | - // a |∞ 2 ` | - // t |∞ 3 ` ` | - // u |∞ 4 - ` ` | - // r |∞ 5 - ` ` | - // d |∞ 6 - ` ` - // a |∞ 7 - ` - // y |∞ 8 - * - // - // Or, effectively, we only need to examine 'threshold - (targetLength - sourceLength)' - // above and below the diagonals. - // - // In practice, when a threshold is provided it is normally capped at '2'. Given that, - // the most around the diagonal we'll ever have to check is +/- 2 elements. i.e. with - // strings of length 10 we'd only check: - // - // a b c d e f g h i j - // ------------------------ - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 7 8 9 10 - // m |∞ 1 * * * - // n |∞ 2 * * * * - // o |∞ 3 * * * * * - // p |∞ 4 * * * * * - // q |∞ 5 * * * * * - // r |∞ 6 * * * * * - // s |∞ 7 * * * * * - // t |∞ 8 * * * * * - // u |∞ 9 * * * * - // v |∞10 * * * - // - // or 10+18+16=44. Or only 44%. if our threshold is two and our strings differ by length - // 2 then we have: - // - // a b c d e f g h - // -------------------- - // |∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ - // |∞ 0 1 2 3 4 5 6 7 8 - // m |∞ 1 * - // n |∞ 2 * * - // o |∞ 3 * * * - // p |∞ 4 * * * - // q |∞ 5 * * * - // r |∞ 6 * * * - // s |∞ 7 * * * - // t |∞ 8 * * * - // u |∞ 9 * * - // v |∞10 * - // - // Then we examine 8+8+8=24 out of 80, or only 30% of the matrix. As the strings - // get larger, the savings increase as well. - - // -------------------------------------------------------------------------------- - - // The highest cost it can be to convert a source to target is targetLength. i.e. - // changing all the characters in source to target (which would be be 'sourceLength' - // changes), and then adding all the missing characters in 'target' (which is - // 'targetLength' - 'sourceLength' changes). Combined that's 'targetLength'. - // - // So we can just cap our threshold here. This makes some of the walking code - // below simpler. - threshold = Math.Min (threshold, targetLength); - - var offset = threshold - minimumEditCount; - Debug.Assert (offset >= 0); - - var matrix = GetMatrix (sourceLength + 2, targetLength + 2); - - var characterToLastSeenIndex_inSource = t_lastSeenIndexPool.Value; - Array.Clear (characterToLastSeenIndex_inSource, 0, LastSeenIndexLength); - - for (var i = 1; i <= sourceLength; i++) { - var lastMatchIndex_inTarget = 0; - var sourceChar = source[i - 1]; - - // Determinethe portion of the column we actually want to examine. - var jStart = Math.Max (1, i - offset); - var jEnd = Math.Min (targetLength, i + minimumEditCount + offset); - - // If we're examining only a subportion of the column, then we need to make sure - // that the values outside that range are set to Infinity. That way we don't - // consider them when we look through edit paths from above (for this column) or - // from the left (for the next column). - if (jStart > 1) { - matrix[i + 1, jStart] = Infinity; - } - - if (jEnd < targetLength) { - matrix[i + 1, jEnd + 2] = Infinity; - } - - for (var j = jStart; j <= jEnd; j++) { - var targetChar = target[j - 1]; - - var i1 = targetChar < LastSeenIndexLength ? characterToLastSeenIndex_inSource[targetChar] : 0; - var j1 = lastMatchIndex_inTarget; - - var matched = sourceChar == targetChar; - if (matched) { - lastMatchIndex_inTarget = j; - } - - matrix[i + 1, j + 1] = Min ( - matrix[i, j] + (matched ? 0 : 1), - matrix[i + 1, j] + 1, - matrix[i, j + 1] + 1, - matrix[i1, j1] + (i - i1 - 1) + 1 + (j - j1 - 1)); - } - - if (sourceChar < LastSeenIndexLength) { - characterToLastSeenIndex_inSource[sourceChar] = i; - } - - // Recall that minimumEditCount is simply the difference in length of our two - // strings. So matrix[i+1,i+1] is the cost for the upper-left diagonal of the - // matrix. matrix[i+1,i+1+minimumEditCount] is the cost for the lower right diagonal. - // Here we are simply getting the lowest cost edit of hese two substrings so far. - // If this lowest cost edit is greater than our threshold, then there is no need - // to proceed. - if (matrix[i + 1, i + minimumEditCount + 1] > threshold) { - return BeyondThreshold; - } - } - - return matrix[sourceLength + 1, targetLength + 1]; - } - - private static string ToString (int[,] matrix, int width, int height) - { - var sb = new StringBuilder (); - for (var j = 0; j < height; j++) { - for (var i = 0; i < width; i++) { - var v = matrix[i + 2, j + 2]; - sb.Append ((v == Infinity ? "∞" : v.ToString ()) + " "); - } - sb.AppendLine (); - } - - return sb.ToString ().Trim (); - } - - private static int GetValue (Dictionary da, char c) - { - return da.TryGetValue (c, out var value) ? value : 0; - } - - private static int Min (int v1, int v2, int v3, int v4) - { - Debug.Assert (v1 >= 0); - Debug.Assert (v2 >= 0); - Debug.Assert (v3 >= 0); - Debug.Assert (v4 >= 0); - - var min = v1; - if (v2 < min) { - min = v2; - } - - if (v3 < min) { - min = v3; - } - - if (v4 < min) { - min = v4; - } - - Debug.Assert (min >= 0); - return min; - } - - private static void SetValue (int[,] matrix, int i, int j, int val) - { - // Matrix is -1 based, so we add 1 to both i and j to make it - // possible to index into the actual storage. - matrix[i + 1, j + 1] = val; - } - } - - internal class SimplePool where T : class - { - private readonly object _gate = new object (); - private readonly Stack _values = new Stack (); - private readonly Func _allocate; - - public SimplePool (Func allocate) - { - _allocate = allocate; - } - - public T Allocate () - { - lock (_gate) { - if (_values.Count > 0) { - return _values.Pop (); - } - - return _allocate (); - } - } - - public void Free (T value) - { - lock (_gate) { - _values.Push (value); - } - } - } - - internal static class ArrayPool - { - private const int MaxPooledArraySize = 256; - - // Keep around a few arrays of size 256 that we can use for operations without - // causing lots of garbage to be created. If we do compare items larger than - // that, then we will just allocate and release those arrays on demand. - private static SimplePool s_pool = new SimplePool (() => new T[MaxPooledArraySize]); - - public static T[] GetArray (int size) - { - if (size <= MaxPooledArraySize) { - var array = s_pool.Allocate (); - Array.Clear (array, 0, array.Length); - return array; - } - - return new T[size]; - } - - public static void ReleaseArray (T[] array) - { - if (array.Length <= MaxPooledArraySize) { - s_pool.Free (array); - } - } - } -} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Hash.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Hash.cs deleted file mode 100644 index 87ea875a..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/Hash.cs +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; - -namespace Roslyn.Utilities -{ - internal static class Hash - { - /// - /// This is how VB Anonymous Types combine hash values for fields. - /// - internal static int Combine(int newKey, int currentKey) - { - return unchecked((currentKey * (int)0xA5555529) + newKey); - } - - internal static int Combine(bool newKeyPart, int currentKey) - { - return Combine(currentKey, newKeyPart ? 1 : 0); - } - - /// - /// This is how VB Anonymous Types combine hash values for fields. - /// PERF: Do not use with enum types because that involves multiple - /// unnecessary boxing operations. Unfortunately, we can't constrain - /// T to "non-enum", so we'll use a more restrictive constraint. - /// - internal static int Combine(T newKeyPart, int currentKey) where T : class? - { - int hash = unchecked(currentKey * (int)0xA5555529); - - if (newKeyPart != null) - { - return unchecked(hash + newKeyPart.GetHashCode()); - } - - return hash; - } - - internal static int CombineValues(IEnumerable? values, int maxItemsToHash = int.MaxValue) - { - if (values == null) - { - return 0; - } - - var hashCode = 0; - var count = 0; - foreach (var value in values) - { - if (count++ >= maxItemsToHash) - { - break; - } - - // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). - if (value != null) - { - hashCode = Hash.Combine(value.GetHashCode(), hashCode); - } - } - - return hashCode; - } - - internal static int CombineValues(T[]? values, int maxItemsToHash = int.MaxValue) - { - if (values == null) - { - return 0; - } - - var maxSize = Math.Min(maxItemsToHash, values.Length); - var hashCode = 0; - - for (int i = 0; i < maxSize; i++) - { - T value = values[i]; - - // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). - if (value != null) - { - hashCode = Hash.Combine(value.GetHashCode(), hashCode); - } - } - - return hashCode; - } - - internal static int CombineValues(ImmutableArray values, int maxItemsToHash = int.MaxValue) - { - if (values.IsDefaultOrEmpty) - { - return 0; - } - - var hashCode = 0; - var count = 0; - foreach (var value in values) - { - if (count++ >= maxItemsToHash) - { - break; - } - - // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). - if (value != null) - { - hashCode = Hash.Combine(value.GetHashCode(), hashCode); - } - } - - return hashCode; - } - - internal static int CombineValues(IEnumerable? values, StringComparer stringComparer, int maxItemsToHash = int.MaxValue) - { - if (values == null) - { - return 0; - } - - var hashCode = 0; - var count = 0; - foreach (var value in values) - { - if (count++ >= maxItemsToHash) - { - break; - } - - if (value != null) - { - hashCode = Hash.Combine(stringComparer.GetHashCode(value), hashCode); - } - } - - return hashCode; - } - - /// - /// The offset bias value used in the FNV-1a algorithm - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - internal const int FnvOffsetBias = unchecked((int)2166136261); - - /// - /// The generative factor used in the FNV-1a algorithm - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - internal const int FnvPrime = 16777619; - - /// - /// Compute the FNV-1a hash of a sequence of bytes - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The sequence of bytes - /// The FNV-1a hash of - internal static int GetFNVHashCode(byte[] data) - { - int hashCode = Hash.FnvOffsetBias; - - for (int i = 0; i < data.Length; i++) - { - hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the FNV-1a hash of a sequence of bytes and determines if the byte - /// sequence is valid ASCII and hence the hash code matches a char sequence - /// encoding the same text. - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The sequence of bytes that are likely to be ASCII text. - /// True if the sequence contains only characters in the ASCII range. - /// The FNV-1a hash of - internal static int GetFNVHashCode(ReadOnlySpan data, out bool isAscii) - { - int hashCode = Hash.FnvOffsetBias; - - byte asciiMask = 0; - - for (int i = 0; i < data.Length; i++) - { - byte b = data[i]; - asciiMask |= b; - hashCode = unchecked((hashCode ^ b) * Hash.FnvPrime); - } - - isAscii = (asciiMask & 0x80) == 0; - return hashCode; - } - - /// - /// Compute the FNV-1a hash of a sequence of bytes - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The sequence of bytes - /// The FNV-1a hash of - internal static int GetFNVHashCode(ImmutableArray data) - { - int hashCode = Hash.FnvOffsetBias; - - for (int i = 0; i < data.Length; i++) - { - hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the hashcode of a sub-string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here - /// for 16-bit Unicode chars on the understanding that the majority of chars will - /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits - /// for generating hash codes. - /// - internal static int GetFNVHashCode(ReadOnlySpan data) - { - int hashCode = Hash.FnvOffsetBias; - - for (int i = 0; i < data.Length; i++) - { - hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the hashcode of a sub-string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here - /// for 16-bit Unicode chars on the understanding that the majority of chars will - /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits - /// for generating hash codes. - /// - /// The input string - /// The start index of the first character to hash - /// The number of characters, beginning with to hash - /// The FNV-1a hash code of the substring beginning at and ending after characters. - internal static int GetFNVHashCode(string text, int start, int length) - => GetFNVHashCode(text.AsSpan(start, length)); - - internal static int GetCaseInsensitiveFNVHashCode(string text) - { - return GetCaseInsensitiveFNVHashCode(text, 0, text.Length); - } - - internal static int GetCaseInsensitiveFNVHashCode(string text, int start, int length) - { - int hashCode = Hash.FnvOffsetBias; - int end = start + length; - - for (int i = start; i < end; i++) - { - hashCode = unchecked((hashCode ^ CaseInsensitiveComparison.ToLower(text[i])) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the hashcode of a sub-string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The input string - /// The start index of the first character to hash - /// The FNV-1a hash code of the substring beginning at and ending at the end of the string. - internal static int GetFNVHashCode(string text, int start) - { - return GetFNVHashCode(text, start, length: text.Length - start); - } - - /// - /// Compute the hashcode of a string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The input string - /// The FNV-1a hash code of - internal static int GetFNVHashCode(string text) - { - return CombineFNVHash(Hash.FnvOffsetBias, text); - } - - /// - /// Compute the hashcode of a string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The input string - /// The FNV-1a hash code of - internal static int GetFNVHashCode(System.Text.StringBuilder text) - { - int hashCode = Hash.FnvOffsetBias; - int end = text.Length; - - for (int i = 0; i < end; i++) - { - hashCode = unchecked((hashCode ^ text[i]) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the hashcode of a sub string using FNV-1a - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The input string as a char array - /// The start index of the first character to hash - /// The number of characters, beginning with to hash - /// The FNV-1a hash code of the substring beginning at and ending after characters. - internal static int GetFNVHashCode(char[] text, int start, int length) - { - int hashCode = Hash.FnvOffsetBias; - int end = start + length; - - for (int i = start; i < end; i++) - { - hashCode = unchecked((hashCode ^ text[i]) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Compute the hashcode of a single character using the FNV-1a algorithm - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// Note: In general, this isn't any more useful than "char.GetHashCode". However, - /// it may be needed if you need to generate the same hash code as a string or - /// substring with just a single character. - /// - /// The character to hash - /// The FNV-1a hash code of the character. - internal static int GetFNVHashCode(char ch) - { - return Hash.CombineFNVHash(Hash.FnvOffsetBias, ch); - } - - /// - /// Combine a string with an existing FNV-1a hash code - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The accumulated hash code - /// The string to combine - /// The result of combining with using the FNV-1a algorithm - internal static int CombineFNVHash(int hashCode, string text) - { - foreach (char ch in text) - { - hashCode = unchecked((hashCode ^ ch) * Hash.FnvPrime); - } - - return hashCode; - } - - /// - /// Combine a char with an existing FNV-1a hash code - /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - /// - /// The accumulated hash code - /// The new character to combine - /// The result of combining with using the FNV-1a algorithm - internal static int CombineFNVHash(int hashCode, char ch) - { - return unchecked((hashCode ^ ch) * Hash.FnvPrime); - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IChecksummedObject.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IChecksummedObject.cs deleted file mode 100644 index f1598b32..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IChecksummedObject.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.CodeAnalysis -{ - /// - /// Indicates whether a type has checksum or not - /// - internal interface IChecksummedObject - { - Checksum Checksum { get; } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IObjectWritable.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IObjectWritable.cs deleted file mode 100644 index 3a331c4f..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/IObjectWritable.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Roslyn.Utilities -{ - /// - /// Objects that implement this interface know how to write their contents to an , - /// so they can be reconstructed later by an . - /// - internal interface IObjectWritable - { - void WriteTo(ObjectWriter writer); - - /// - /// Returns 'true' when the same instance could be used more than once. - /// Instances that return 'false' should not be tracked for the purpose - /// of de-duplication while serializing/deserializing. - /// - bool ShouldReuseInSerialization { get; } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinder.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinder.cs deleted file mode 100644 index 13c884f2..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinder.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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.Generic; - -namespace Roslyn.Utilities -{ - /// - /// is a registry that maps between arbitrary s and - /// the 'reader' function used to deserialize serialized instances of those types. Registration - /// must happen ahead of time using the method. - /// - internal static class ObjectBinder - { - /// - /// Lock for all data in this type. - /// - private static object s_gate = new object(); - - /// - /// Last created snapshot of our data. We hand this out instead of exposing our raw - /// data so that and do not need to - /// take any locks while processing. - /// - private static ObjectBinderSnapshot? s_lastSnapshot = null; - - /// - /// Map from a to the corresponding index in and - /// . will write out the index into - /// the stream, and will use that index to get the reader used - /// for deserialization. - /// - private static readonly Dictionary s_typeToIndex = new Dictionary(); - private static readonly List s_types = new List(); - private static readonly List> s_typeReaders = new List>(); - - /// - /// Gets an immutable copy of the state of this binder. This copy does not need to be - /// locked while it is used. - /// - public static ObjectBinderSnapshot GetSnapshot() - { - lock (s_gate) - { - if (s_lastSnapshot == null) - { - s_lastSnapshot = new ObjectBinderSnapshot(s_typeToIndex, s_types, s_typeReaders); - } - - return s_lastSnapshot.Value; - } - } - - public static void RegisterTypeReader(Type type, Func typeReader) - { - lock (s_gate) - { - if (s_typeToIndex.ContainsKey(type)) - { - // We already knew about this type, nothing to register. - return; - } - - int index = s_typeReaders.Count; - s_types.Add(type); - s_typeReaders.Add(typeReader); - s_typeToIndex.Add(type, index); - - // Registering this type mutated state, clear the cached last snapshot as it - // is no longer valid. - s_lastSnapshot = null; - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinderSnapshot.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinderSnapshot.cs deleted file mode 100644 index 8ccf8c7b..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectBinderSnapshot.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Generic; -using System.Collections.Immutable; - -namespace Roslyn.Utilities -{ - internal readonly struct ObjectBinderSnapshot - { - private readonly Dictionary _typeToIndex; - private readonly ImmutableArray _types; - private readonly ImmutableArray> _typeReaders; - - public ObjectBinderSnapshot( - Dictionary typeToIndex, - List types, - List> typeReaders) - { - _typeToIndex = new Dictionary(typeToIndex); - _types = types.ToImmutableArray(); - _typeReaders = typeReaders.ToImmutableArray(); - } - - public int GetTypeId(Type type) - => _typeToIndex[type]; - - public Type GetTypeFromId(int typeId) - => _types[typeId]; - - public Func GetTypeReaderFromId(int typeId) - => _typeReaders[typeId]; - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectPool`1.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectPool`1.cs deleted file mode 100644 index 2f03191a..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectPool`1.cs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -// define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will -// make everything about 2-3x slower -// -// #define TRACE_LEAKS - -// define DETECT_LEAKS to detect possible leaks -// #if DEBUG -// #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug. -// #endif - -using System; -using System.Diagnostics; -using System.Threading; - -#if DETECT_LEAKS -using System.Runtime.CompilerServices; - -#endif -namespace Microsoft.CodeAnalysis.PooledObjects -{ - /// - /// Generic implementation of object pooling pattern with predefined pool size limit. The main - /// purpose is that limited number of frequently used objects can be kept in the pool for - /// further recycling. - /// - /// Notes: - /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there - /// is no space in the pool, extra returned objects will be dropped. - /// - /// 2) it is implied that if object was obtained from a pool, the caller will return it back in - /// a relatively short time. Keeping checked out objects for long durations is ok, but - /// reduces usefulness of pooling. Just new up your own. - /// - /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. - /// Rationale: - /// If there is no intent for reusing the object, do not use pool - just use "new". - /// - internal class ObjectPool where T : class - { - [DebuggerDisplay("{Value,nq}")] - private struct Element - { - internal T Value; - } - - /// - /// Not using System.Func{T} because this file is linked into the (debugger) Formatter, - /// which does not have that type (since it compiles against .NET 2.0). - /// - internal delegate T Factory(); - - // Storage for the pool objects. The first item is stored in a dedicated field because we - // expect to be able to satisfy most requests from it. - private T _firstItem; - private readonly Element[] _items; - - // factory is stored for the lifetime of the pool. We will call this only when pool needs to - // expand. compared to "new T()", Func gives more flexibility to implementers and faster - // than "new T()". - private readonly Factory _factory; - -#if DETECT_LEAKS - private static readonly ConditionalWeakTable leakTrackers = new ConditionalWeakTable(); - - private class LeakTracker : IDisposable - { - private volatile bool disposed; - -#if TRACE_LEAKS - internal volatile object Trace = null; -#endif - - public void Dispose() - { - disposed = true; - GC.SuppressFinalize(this); - } - - private string GetTrace() - { -#if TRACE_LEAKS - return Trace == null ? "" : Trace.ToString(); -#else - return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n"; -#endif - } - - ~LeakTracker() - { - if (!this.disposed && !Environment.HasShutdownStarted) - { - var trace = GetTrace(); - - // If you are seeing this message it means that object has been allocated from the pool - // and has not been returned back. This is not critical, but turns pool into rather - // inefficient kind of "new". - Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END"); - } - } - } -#endif - - internal ObjectPool(Factory factory) - : this(factory, Environment.ProcessorCount * 2) - { } - - internal ObjectPool(Factory factory, int size) - { - Debug.Assert(size >= 1); - _factory = factory; - _items = new Element[size - 1]; - } - - private T CreateInstance() - { - var inst = _factory(); - return inst; - } - - /// - /// Produces an instance. - /// - /// - /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. - /// Note that Free will try to store recycled objects close to the start thus statistically - /// reducing how far we will typically search. - /// - internal T Allocate() - { - // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. - // Note that the initial read is optimistically not synchronized. That is intentional. - // We will interlock only when we have a candidate. in a worst case we may miss some - // recently returned objects. Not a big deal. - var inst = _firstItem; - if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst)) - { - inst = AllocateSlow(); - } - -#if DETECT_LEAKS - var tracker = new LeakTracker(); - leakTrackers.Add(inst, tracker); - -#if TRACE_LEAKS - var frame = CaptureStackTrace(); - tracker.Trace = frame; -#endif -#endif - return inst; - } - - private T AllocateSlow() - { - var items = _items; - - for (var i = 0; i < items.Length; i++) - { - // Note that the initial read is optimistically not synchronized. That is intentional. - // We will interlock only when we have a candidate. in a worst case we may miss some - // recently returned objects. Not a big deal. - var inst = items[i].Value; - if (inst != null) - { - if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst)) - { - return inst; - } - } - } - - return CreateInstance(); - } - - /// - /// Returns objects to the pool. - /// - /// - /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. - /// Note that Free will try to store recycled objects close to the start thus statistically - /// reducing how far we will typically search in Allocate. - /// - internal void Free(T obj) - { - Validate(obj); - ForgetTrackedObject(obj); - - if (_firstItem == null) - { - // Intentionally not using interlocked here. - // In a worst case scenario two objects may be stored into same slot. - // It is very unlikely to happen and will only mean that one of the objects will get collected. - _firstItem = obj; - } - else - { - FreeSlow(obj); - } - } - - private void FreeSlow(T obj) - { - var items = _items; - for (var i = 0; i < items.Length; i++) - { - if (items[i].Value == null) - { - // Intentionally not using interlocked here. - // In a worst case scenario two objects may be stored into same slot. - // It is very unlikely to happen and will only mean that one of the objects will get collected. - items[i].Value = obj; - break; - } - } - } - - /// - /// Removes an object from leak tracking. - /// - /// This is called when an object is returned to the pool. It may also be explicitly - /// called if an object allocated from the pool is intentionally not being returned - /// to the pool. This can be of use with pooled arrays if the consumer wants to - /// return a larger array to the pool than was originally allocated. - /// - [Conditional("DEBUG")] - internal void ForgetTrackedObject(T old, T replacement = null) - { -#if DETECT_LEAKS - LeakTracker tracker; - if (leakTrackers.TryGetValue(old, out tracker)) - { - tracker.Dispose(); - leakTrackers.Remove(old); - } - else - { - var trace = CaptureStackTrace(); - Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END"); - } - - if (replacement != null) - { - tracker = new LeakTracker(); - leakTrackers.Add(replacement, tracker); - } -#endif - } - -#if DETECT_LEAKS - private static Lazy _stackTraceType = new Lazy(() => Type.GetType("System.Diagnostics.StackTrace")); - - private static object CaptureStackTrace() - { - return Activator.CreateInstance(_stackTraceType.Value); - } -#endif - - [Conditional("DEBUG")] - private void Validate(object obj) - { - Debug.Assert(obj != null, "freeing null?"); - - Debug.Assert(_firstItem != obj, "freeing twice?"); - - var items = _items; - for (var i = 0; i < items.Length; i++) - { - var value = items[i].Value; - if (value == null) - { - return; - } - - Debug.Assert(value != obj, "freeing twice?"); - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectReader.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectReader.cs deleted file mode 100644 index 42007407..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectReader.cs +++ /dev/null @@ -1,637 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis; - -namespace Roslyn.Utilities -{ -#if COMPILERCORE - using Resources = CodeAnalysisResources; -#else - using Resources = WorkspacesResources; -#endif - - using EncodingKind = ObjectWriter.EncodingKind; - - /// - /// An that deserializes objects from a byte stream. - /// - internal sealed partial class ObjectReader : IDisposable - { - /// - /// We start the version at something reasonably random. That way an older file, with - /// some random start-bytes, has little chance of matching our version. When incrementing - /// this version, just change VersionByte2. - /// - internal const byte VersionByte1 = 0b10101010; - internal const byte VersionByte2 = 0b00001001; - - private readonly BinaryReader _reader; - private readonly CancellationToken _cancellationToken; - - /// - /// Map of reference id's to deserialized objects. - /// - /// These are not readonly because they're structs and we mutate them. - /// - private ReaderReferenceMap _objectReferenceMap; - private ReaderReferenceMap _stringReferenceMap; - - /// - /// Copy of the global binder data that maps from Types to the appropriate reading-function - /// for that type. Types register functions directly with , but - /// that means that is both static and locked. This gives us - /// local copy we can work with without needing to worry about anyone else mutating. - /// - private readonly ObjectBinderSnapshot _binderSnapshot; - - private int _recursionDepth; - - /// - /// Creates a new instance of a . - /// - /// The stream to read objects from. - /// - private ObjectReader( - Stream stream, - CancellationToken cancellationToken) - { - // String serialization assumes both reader and writer to be of the same endianness. - // It can be adjusted for BigEndian if needed. - Debug.Assert(BitConverter.IsLittleEndian); - - _reader = new BinaryReader(stream, Encoding.UTF8); - _objectReferenceMap = ReaderReferenceMap.Create(); - _stringReferenceMap = ReaderReferenceMap.Create(); - - // Capture a copy of the current static binder state. That way we don't have to - // access any locks while we're doing our processing. - _binderSnapshot = ObjectBinder.GetSnapshot(); - - _cancellationToken = cancellationToken; - } - - /// - /// Attempts to create a from the provided . - /// If the does not start with a valid header, then will - /// be returned. - /// - public static ObjectReader TryGetReader( - Stream stream, - CancellationToken cancellationToken = default) - { - if (stream == null) - { - return null; - } - - if (stream.ReadByte() != VersionByte1 || - stream.ReadByte() != VersionByte2) - { - return null; - } - - return new ObjectReader(stream, cancellationToken); - } - - public void Dispose() - { - _objectReferenceMap.Dispose(); - _stringReferenceMap.Dispose(); - _recursionDepth = 0; - } - - public bool ReadBoolean() => _reader.ReadBoolean(); - public byte ReadByte() => _reader.ReadByte(); - // read as ushort because BinaryWriter fails on chars that are unicode surrogates - public char ReadChar() => (char)_reader.ReadUInt16(); - public decimal ReadDecimal() => _reader.ReadDecimal(); - public double ReadDouble() => _reader.ReadDouble(); - public float ReadSingle() => _reader.ReadSingle(); - public int ReadInt32() => _reader.ReadInt32(); - public long ReadInt64() => _reader.ReadInt64(); - public sbyte ReadSByte() => _reader.ReadSByte(); - public short ReadInt16() => _reader.ReadInt16(); - public uint ReadUInt32() => _reader.ReadUInt32(); - public ulong ReadUInt64() => _reader.ReadUInt64(); - public ushort ReadUInt16() => _reader.ReadUInt16(); - public string ReadString() => ReadStringValue(); - - public Guid ReadGuid() - { - var accessor = new ObjectWriter.GuidAccessor - { - Low64 = ReadInt64(), - High64 = ReadInt64() - }; - - return accessor.Guid; - } - - public object ReadValue() - { - var oldDepth = _recursionDepth; - _recursionDepth++; - - object value; - if (_recursionDepth % ObjectWriter.MaxRecursionDepth == 0) - { - // If we're recursing too deep, move the work to another thread to do so we - // don't blow the stack. - var task = Task.Factory.StartNew( - () => ReadValueWorker(), - _cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - // We must not proceed until the additional task completes. After returning from a read, the underlying - // stream providing access to raw memory will be closed; if this occurs before the separate thread - // completes its read then an access violation can occur attempting to read from unmapped memory. - // - // CANCELLATION: If cancellation is required, DO NOT attempt to cancel the operation by cancelling this - // wait. Cancellation must only be implemented by modifying 'task' to cancel itself in a timely manner - // so the wait can complete. - value = task.GetAwaiter().GetResult(); - } - else - { - value = ReadValueWorker(); - } - - _recursionDepth--; - Debug.Assert(oldDepth == _recursionDepth); - - return value; - } - - private object ReadValueWorker() - { - var kind = (EncodingKind)_reader.ReadByte(); - switch (kind) - { - case EncodingKind.Null: return null; - case EncodingKind.Boolean_True: return true; - case EncodingKind.Boolean_False: return false; - case EncodingKind.Int8: return _reader.ReadSByte(); - case EncodingKind.UInt8: return _reader.ReadByte(); - case EncodingKind.Int16: return _reader.ReadInt16(); - case EncodingKind.UInt16: return _reader.ReadUInt16(); - case EncodingKind.Int32: return _reader.ReadInt32(); - case EncodingKind.Int32_1Byte: return (int)_reader.ReadByte(); - case EncodingKind.Int32_2Bytes: return (int)_reader.ReadUInt16(); - case EncodingKind.Int32_0: - case EncodingKind.Int32_1: - case EncodingKind.Int32_2: - case EncodingKind.Int32_3: - case EncodingKind.Int32_4: - case EncodingKind.Int32_5: - case EncodingKind.Int32_6: - case EncodingKind.Int32_7: - case EncodingKind.Int32_8: - case EncodingKind.Int32_9: - case EncodingKind.Int32_10: - return (int)kind - (int)EncodingKind.Int32_0; - case EncodingKind.UInt32: return _reader.ReadUInt32(); - case EncodingKind.UInt32_1Byte: return (uint)_reader.ReadByte(); - case EncodingKind.UInt32_2Bytes: return (uint)_reader.ReadUInt16(); - case EncodingKind.UInt32_0: - case EncodingKind.UInt32_1: - case EncodingKind.UInt32_2: - case EncodingKind.UInt32_3: - case EncodingKind.UInt32_4: - case EncodingKind.UInt32_5: - case EncodingKind.UInt32_6: - case EncodingKind.UInt32_7: - case EncodingKind.UInt32_8: - case EncodingKind.UInt32_9: - case EncodingKind.UInt32_10: - return (uint)((int)kind - (int)EncodingKind.UInt32_0); - case EncodingKind.Int64: return _reader.ReadInt64(); - case EncodingKind.UInt64: return _reader.ReadUInt64(); - case EncodingKind.Float4: return _reader.ReadSingle(); - case EncodingKind.Float8: return _reader.ReadDouble(); - case EncodingKind.Decimal: return _reader.ReadDecimal(); - case EncodingKind.Char: - // read as ushort because BinaryWriter fails on chars that are unicode surrogates - return (char)_reader.ReadUInt16(); - case EncodingKind.StringUtf8: - case EncodingKind.StringUtf16: - case EncodingKind.StringRef_4Bytes: - case EncodingKind.StringRef_1Byte: - case EncodingKind.StringRef_2Bytes: - return ReadStringValue(kind); - case EncodingKind.ObjectRef_4Bytes: return _objectReferenceMap.GetValue(_reader.ReadInt32()); - case EncodingKind.ObjectRef_1Byte: return _objectReferenceMap.GetValue(_reader.ReadByte()); - case EncodingKind.ObjectRef_2Bytes: return _objectReferenceMap.GetValue(_reader.ReadUInt16()); - case EncodingKind.Object: return ReadObject(); - case EncodingKind.DateTime: return DateTime.FromBinary(_reader.ReadInt64()); - case EncodingKind.Array: - case EncodingKind.Array_0: - case EncodingKind.Array_1: - case EncodingKind.Array_2: - case EncodingKind.Array_3: - return ReadArray(kind); - default: - throw ExceptionUtilities.UnexpectedValue(kind); - } - } - - /// - /// An reference-id to object map, that can share base data efficiently. - /// - private struct ReaderReferenceMap where T : class - { - private readonly List _values; - - internal static readonly ObjectPool> s_objectListPool - = new ObjectPool>(() => new List(20)); - - private ReaderReferenceMap(List values) - => _values = values; - - public static ReaderReferenceMap Create() - => new ReaderReferenceMap(s_objectListPool.Allocate()); - - public void Dispose() - { - _values.Clear(); - s_objectListPool.Free(_values); - } - - - public int GetNextObjectId() - { - var id = _values.Count; - _values.Add(null); - return id; - } - - public void AddValue(T value) - => _values.Add(value); - - public void AddValue(int index, T value) - => _values[index] = value; - - public T GetValue(int referenceId) - => _values[referenceId]; - } - - internal uint ReadCompressedUInt() - { - var info = _reader.ReadByte(); - byte marker = (byte)(info & ObjectWriter.ByteMarkerMask); - byte byte0 = (byte)(info & ~ObjectWriter.ByteMarkerMask); - - if (marker == ObjectWriter.Byte1Marker) - { - return byte0; - } - - if (marker == ObjectWriter.Byte2Marker) - { - var byte1 = _reader.ReadByte(); - return (((uint)byte0) << 8) | byte1; - } - - if (marker == ObjectWriter.Byte4Marker) - { - var byte1 = _reader.ReadByte(); - var byte2 = _reader.ReadByte(); - var byte3 = _reader.ReadByte(); - - return (((uint)byte0) << 24) | (((uint)byte1) << 16) | (((uint)byte2) << 8) | byte3; - } - - throw ExceptionUtilities.UnexpectedValue(marker); - } - - private string ReadStringValue() - { - var kind = (EncodingKind)_reader.ReadByte(); - return kind == EncodingKind.Null ? null : ReadStringValue(kind); - } - - private string ReadStringValue(EncodingKind kind) - { - switch (kind) - { - case EncodingKind.StringRef_1Byte: - return _stringReferenceMap.GetValue(_reader.ReadByte()); - - case EncodingKind.StringRef_2Bytes: - return _stringReferenceMap.GetValue(_reader.ReadUInt16()); - - case EncodingKind.StringRef_4Bytes: - return _stringReferenceMap.GetValue(_reader.ReadInt32()); - - case EncodingKind.StringUtf16: - case EncodingKind.StringUtf8: - return ReadStringLiteral(kind); - - default: - throw ExceptionUtilities.UnexpectedValue(kind); - } - } - - private unsafe string ReadStringLiteral(EncodingKind kind) - { - string value; - if (kind == EncodingKind.StringUtf8) - { - value = _reader.ReadString(); - } - else - { - // This is rare, just allocate UTF16 bytes for simplicity. - int characterCount = (int)ReadCompressedUInt(); - byte[] bytes = _reader.ReadBytes(characterCount * sizeof(char)); - fixed (byte* bytesPtr = bytes) - { - value = new string((char*)bytesPtr, 0, characterCount); - } - } - - _stringReferenceMap.AddValue(value); - return value; - } - - private Array ReadArray(EncodingKind kind) - { - int length; - switch (kind) - { - case EncodingKind.Array_0: - length = 0; - break; - case EncodingKind.Array_1: - length = 1; - break; - case EncodingKind.Array_2: - length = 2; - break; - case EncodingKind.Array_3: - length = 3; - break; - default: - length = (int)this.ReadCompressedUInt(); - break; - } - - // SUBTLE: If it was a primitive array, only the EncodingKind byte of the element type was written, instead of encoding as a type. - var elementKind = (EncodingKind)_reader.ReadByte(); - - var elementType = ObjectWriter.s_reverseTypeMap[(int)elementKind]; - if (elementType != null) - { - return this.ReadPrimitiveTypeArrayElements(elementType, elementKind, length); - } - else - { - // custom type case - elementType = this.ReadTypeAfterTag(); - - // recursive: create instance and read elements next in stream - Array array = Array.CreateInstance(elementType, length); - - for (int i = 0; i < length; ++i) - { - var value = this.ReadValue(); - array.SetValue(value, i); - } - - return array; - } - } - - private Array ReadPrimitiveTypeArrayElements(Type type, EncodingKind kind, int length) - { - Debug.Assert(ObjectWriter.s_reverseTypeMap[(int)kind] == type); - - // optimizations for supported array type by binary reader - if (type == typeof(byte)) { return _reader.ReadBytes(length); } - if (type == typeof(char)) { return _reader.ReadChars(length); } - - // optimizations for string where object reader/writer has its own mechanism to - // reduce duplicated strings - if (type == typeof(string)) { return ReadStringArrayElements(CreateArray(length)); } - if (type == typeof(bool)) { return ReadBooleanArrayElements(CreateArray(length)); } - - // otherwise, read elements directly from underlying binary writer - switch (kind) - { - case EncodingKind.Int8: return ReadInt8ArrayElements(CreateArray(length)); - case EncodingKind.Int16: return ReadInt16ArrayElements(CreateArray(length)); - case EncodingKind.Int32: return ReadInt32ArrayElements(CreateArray(length)); - case EncodingKind.Int64: return ReadInt64ArrayElements(CreateArray(length)); - case EncodingKind.UInt16: return ReadUInt16ArrayElements(CreateArray(length)); - case EncodingKind.UInt32: return ReadUInt32ArrayElements(CreateArray(length)); - case EncodingKind.UInt64: return ReadUInt64ArrayElements(CreateArray(length)); - case EncodingKind.Float4: return ReadFloat4ArrayElements(CreateArray(length)); - case EncodingKind.Float8: return ReadFloat8ArrayElements(CreateArray(length)); - case EncodingKind.Decimal: return ReadDecimalArrayElements(CreateArray(length)); - default: - throw ExceptionUtilities.UnexpectedValue(kind); - } - } - - private bool[] ReadBooleanArrayElements(bool[] array) - { - // Confirm the type to be read below is ulong - Debug.Assert(BitVector.BitsPerWord == 64); - - var wordLength = BitVector.WordsRequired(array.Length); - - var count = 0; - for (var i = 0; i < wordLength; i++) - { - var word = _reader.ReadUInt64(); - - for (var p = 0; p < BitVector.BitsPerWord; p++) - { - if (count >= array.Length) - { - return array; - } - - array[count++] = BitVector.IsTrue(word, p); - } - } - - return array; - } - - private static T[] CreateArray(int length) - { - if (length == 0) - { - // quick check - return Array.Empty(); - } - else - { - return new T[length]; - } - } - - private string[] ReadStringArrayElements(string[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = this.ReadStringValue(); - } - - return array; - } - - private sbyte[] ReadInt8ArrayElements(sbyte[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadSByte(); - } - - return array; - } - - private short[] ReadInt16ArrayElements(short[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadInt16(); - } - - return array; - } - - private int[] ReadInt32ArrayElements(int[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadInt32(); - } - - return array; - } - - private long[] ReadInt64ArrayElements(long[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadInt64(); - } - - return array; - } - - private ushort[] ReadUInt16ArrayElements(ushort[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadUInt16(); - } - - return array; - } - - private uint[] ReadUInt32ArrayElements(uint[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadUInt32(); - } - - return array; - } - - private ulong[] ReadUInt64ArrayElements(ulong[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadUInt64(); - } - - return array; - } - - private decimal[] ReadDecimalArrayElements(decimal[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadDecimal(); - } - - return array; - } - - private float[] ReadFloat4ArrayElements(float[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadSingle(); - } - - return array; - } - - private double[] ReadFloat8ArrayElements(double[] array) - { - for (var i = 0; i < array.Length; i++) - { - array[i] = _reader.ReadDouble(); - } - - return array; - } - - public Type ReadType() - { - _reader.ReadByte(); - return Type.GetType(ReadString()); - } - - private Type ReadTypeAfterTag() - => _binderSnapshot.GetTypeFromId(this.ReadInt32()); - - private object ReadObject() - { - var objectId = _objectReferenceMap.GetNextObjectId(); - - // reading an object may recurse. So we need to grab our ID up front as we'll - // end up making our sub-objects before we make this object. - - var typeReader = _binderSnapshot.GetTypeReaderFromId(this.ReadInt32()); - - // recursive: read and construct instance immediately from member elements encoding next in the stream - var instance = typeReader(this); - - if (instance.ShouldReuseInSerialization) - { - _objectReferenceMap.AddValue(objectId, instance); - } - - return instance; - } - - private static Exception DeserializationReadIncorrectNumberOfValuesException(string typeName) - { - throw new InvalidOperationException(String.Format(Resources.Deserialization_reader_for_0_read_incorrect_number_of_values, typeName)); - } - - private static Exception NoSerializationTypeException(string typeName) - { - return new InvalidOperationException(string.Format(Resources.The_type_0_is_not_understood_by_the_serialization_binder, typeName)); - } - - private static Exception NoSerializationReaderException(string typeName) - { - return new InvalidOperationException(string.Format(Resources.Cannot_serialize_type_0, typeName)); - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectWriter.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectWriter.cs deleted file mode 100644 index e86e0968..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ObjectWriter.cs +++ /dev/null @@ -1,1189 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis; - -namespace Roslyn.Utilities -{ - using System.Collections.Immutable; - using System.Threading.Tasks; -#if COMPILERCORE - using Resources = CodeAnalysisResources; -#else - using Resources = WorkspacesResources; -#endif - - /// - /// An that serializes objects to a byte stream. - /// - internal sealed partial class ObjectWriter : IDisposable - { - private readonly BinaryWriter _writer; - private readonly CancellationToken _cancellationToken; - - /// - /// Map of serialized object's reference ids. The object-reference-map uses reference equality - /// for performance. While the string-reference-map uses value-equality for greater cache hits - /// and reuse. - /// - /// These are not readonly because they're structs and we mutate them. - /// - /// When we write out objects/strings we give each successive, unique, item a monotonically - /// increasing integral ID starting at 0. I.e. the first object gets ID-0, the next gets - /// ID-1 and so on and so forth. We do *not* include these IDs with the object when it is - /// written out. We only include the ID if we hit the object *again* while writing. - /// - /// During reading, the reader knows to give each object it reads the same monotonically - /// increasing integral value. i.e. the first object it reads is put into an array at position - /// 0, the next at position 1, and so on. Then, when the reader reads in an object-reference - /// it can just retrieved it directly from that array. - /// - /// In other words, writing and reading take advantage of the fact that they know they will - /// write and read objects in the exact same order. So they only need the IDs for references - /// and not the objects themselves because the ID is inferred from the order the object is - /// written or read in. - /// - private WriterReferenceMap _objectReferenceMap; - private WriterReferenceMap _stringReferenceMap; - - /// - /// Copy of the global binder data that maps from Types to the appropriate reading-function - /// for that type. Types register functions directly with , but - /// that means that is both static and locked. This gives us - /// local copy we can work with without needing to worry about anyone else mutating. - /// - private readonly ObjectBinderSnapshot _binderSnapshot; - - private int _recursionDepth; - internal const int MaxRecursionDepth = 50; - - /// - /// Creates a new instance of a . - /// - /// The stream to write to. - /// - public ObjectWriter( - Stream stream, - CancellationToken cancellationToken = default) - { - // String serialization assumes both reader and writer to be of the same endianness. - // It can be adjusted for BigEndian if needed. - Debug.Assert(BitConverter.IsLittleEndian); - - _writer = new BinaryWriter(stream, Encoding.UTF8); - _objectReferenceMap = new WriterReferenceMap(valueEquality: false); - _stringReferenceMap = new WriterReferenceMap(valueEquality: true); - _cancellationToken = cancellationToken; - - // Capture a copy of the current static binder state. That way we don't have to - // access any locks while we're doing our processing. - _binderSnapshot = ObjectBinder.GetSnapshot(); - - WriteVersion(); - } - - private void WriteVersion() - { - _writer.Write(ObjectReader.VersionByte1); - _writer.Write(ObjectReader.VersionByte2); - } - - public void Dispose() - { - _objectReferenceMap.Dispose(); - _stringReferenceMap.Dispose(); - _recursionDepth = 0; - } - - public void WriteBoolean(bool value) => _writer.Write(value); - public void WriteByte(byte value) => _writer.Write(value); - // written as ushort because BinaryWriter fails on chars that are unicode surrogates - public void WriteChar(char ch) => _writer.Write((ushort)ch); - public void WriteDecimal(decimal value) => _writer.Write(value); - public void WriteDouble(double value) => _writer.Write(value); - public void WriteSingle(float value) => _writer.Write(value); - public void WriteInt32(int value) => _writer.Write(value); - public void WriteInt64(long value) => _writer.Write(value); - public void WriteSByte(sbyte value) => _writer.Write(value); - public void WriteInt16(short value) => _writer.Write(value); - public void WriteUInt32(uint value) => _writer.Write(value); - public void WriteUInt64(ulong value) => _writer.Write(value); - public void WriteUInt16(ushort value) => _writer.Write(value); - public void WriteString(string value) => WriteStringValue(value); - - /// - /// Used so we can easily grab the low/high 64bits of a guid for serialization. - /// - [StructLayout(LayoutKind.Explicit)] - internal struct GuidAccessor - { - [FieldOffset(0)] - public Guid Guid; - - [FieldOffset(0)] - public long Low64; - [FieldOffset(8)] - public long High64; - } - - public void WriteGuid(Guid guid) - { - var accessor = new GuidAccessor { Guid = guid }; - WriteInt64(accessor.Low64); - WriteInt64(accessor.High64); - } - - public void WriteValue(object value) - { - Debug.Assert(value == null || !value.GetType().GetTypeInfo().IsEnum, "Enum should not be written with WriteValue. Write them as ints instead."); - - if (value == null) - { - _writer.Write((byte)EncodingKind.Null); - return; - } - - var type = value.GetType(); - var typeInfo = type.GetTypeInfo(); - Debug.Assert(!typeInfo.IsEnum, "Enums should not be written with WriteObject. Write them out as integers instead."); - - // Perf: Note that JIT optimizes each expression value.GetType() == typeof(T) to a single register comparison. - // Also the checks are sorted by commonality of the checked types. - - // The primitive types are - // Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, - // Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single. - if (typeInfo.IsPrimitive) - { - // Note: int, double, bool, char, have been chosen to go first as they're they - // common values of literals in code, and so would be hte likely hits if we do - // have a primitive type we're serializing out. - if (value.GetType() == typeof(int)) - { - WriteEncodedInt32((int)value); - } - else if (value.GetType() == typeof(double)) - { - _writer.Write((byte)EncodingKind.Float8); - _writer.Write((double)value); - } - else if (value.GetType() == typeof(bool)) - { - _writer.Write((byte)((bool)value ? EncodingKind.Boolean_True : EncodingKind.Boolean_False)); - } - else if (value.GetType() == typeof(char)) - { - _writer.Write((byte)EncodingKind.Char); - _writer.Write((ushort)(char)value); // written as ushort because BinaryWriter fails on chars that are unicode surrogates - } - else if (value.GetType() == typeof(byte)) - { - _writer.Write((byte)EncodingKind.UInt8); - _writer.Write((byte)value); - } - else if (value.GetType() == typeof(short)) - { - _writer.Write((byte)EncodingKind.Int16); - _writer.Write((short)value); - } - else if (value.GetType() == typeof(long)) - { - _writer.Write((byte)EncodingKind.Int64); - _writer.Write((long)value); - } - else if (value.GetType() == typeof(sbyte)) - { - _writer.Write((byte)EncodingKind.Int8); - _writer.Write((sbyte)value); - } - else if (value.GetType() == typeof(float)) - { - _writer.Write((byte)EncodingKind.Float4); - _writer.Write((float)value); - } - else if (value.GetType() == typeof(ushort)) - { - _writer.Write((byte)EncodingKind.UInt16); - _writer.Write((ushort)value); - } - else if (value.GetType() == typeof(uint)) - { - WriteEncodedUInt32((uint)value); - } - else if (value.GetType() == typeof(ulong)) - { - _writer.Write((byte)EncodingKind.UInt64); - _writer.Write((ulong)value); - } - else - { - throw ExceptionUtilities.UnexpectedValue(value.GetType()); - } - } - else if (value.GetType() == typeof(decimal)) - { - _writer.Write((byte)EncodingKind.Decimal); - _writer.Write((decimal)value); - } - else if (value.GetType() == typeof(DateTime)) - { - _writer.Write((byte)EncodingKind.DateTime); - _writer.Write(((DateTime)value).ToBinary()); - } - else if (value.GetType() == typeof(string)) - { - WriteStringValue((string)value); - } - else if (type.IsArray) - { - var instance = (Array)value; - - if (instance.Rank > 1) - { - throw new InvalidOperationException(Resources.Arrays_with_more_than_one_dimension_cannot_be_serialized); - } - - WriteArray(instance); - } - else - { - WriteObject(instance: value, instanceAsWritableOpt: null); - } - } - - public void WriteValue(IObjectWritable value) - { - if (value == null) - { - _writer.Write((byte)EncodingKind.Null); - return; - } - - WriteObject(instance: value, instanceAsWritableOpt: value); - } - - private void WriteEncodedInt32(int v) - { - if (v >= 0 && v <= 10) - { - _writer.Write((byte)((int)EncodingKind.Int32_0 + v)); - } - else if (v >= 0 && v < byte.MaxValue) - { - _writer.Write((byte)EncodingKind.Int32_1Byte); - _writer.Write((byte)v); - } - else if (v >= 0 && v < ushort.MaxValue) - { - _writer.Write((byte)EncodingKind.Int32_2Bytes); - _writer.Write((ushort)v); - } - else - { - _writer.Write((byte)EncodingKind.Int32); - _writer.Write(v); - } - } - - private void WriteEncodedUInt32(uint v) - { - if (v >= 0 && v <= 10) - { - _writer.Write((byte)((int)EncodingKind.UInt32_0 + v)); - } - else if (v >= 0 && v < byte.MaxValue) - { - _writer.Write((byte)EncodingKind.UInt32_1Byte); - _writer.Write((byte)v); - } - else if (v >= 0 && v < ushort.MaxValue) - { - _writer.Write((byte)EncodingKind.UInt32_2Bytes); - _writer.Write((ushort)v); - } - else - { - _writer.Write((byte)EncodingKind.UInt32); - _writer.Write(v); - } - } - - /// - /// An object reference to reference-id map, that can share base data efficiently. - /// - private struct WriterReferenceMap - { - private readonly Dictionary _valueToIdMap; - private readonly bool _valueEquality; - private int _nextId; - - private static readonly ObjectPool> s_referenceDictionaryPool = - new ObjectPool>(() => new Dictionary(128, ReferenceEqualityComparer.Instance)); - - private static readonly ObjectPool> s_valueDictionaryPool = - new ObjectPool>(() => new Dictionary(128)); - - public WriterReferenceMap(bool valueEquality) - { - _valueEquality = valueEquality; - _valueToIdMap = GetDictionaryPool(valueEquality).Allocate(); - _nextId = 0; - } - - private static ObjectPool> GetDictionaryPool(bool valueEquality) - => valueEquality ? s_valueDictionaryPool : s_referenceDictionaryPool; - - public void Dispose() - { - var pool = GetDictionaryPool(_valueEquality); - - // If the map grew too big, don't return it to the pool. - // When testing with the Roslyn solution, this dropped only 2.5% of requests. - if (_valueToIdMap.Count > 1024) - { - pool.ForgetTrackedObject(_valueToIdMap); - } - else - { - _valueToIdMap.Clear(); - pool.Free(_valueToIdMap); - } - } - - public bool TryGetReferenceId(object value, out int referenceId) - => _valueToIdMap.TryGetValue(value, out referenceId); - - public void Add(object value, bool isReusable) - { - var id = _nextId++; - - if (isReusable) - { - _valueToIdMap.Add(value, id); - } - } - } - - internal void WriteCompressedUInt(uint value) - { - if (value <= (byte.MaxValue >> 2)) - { - _writer.Write((byte)value); - } - else if (value <= (ushort.MaxValue >> 2)) - { - byte byte0 = (byte)(((value >> 8) & 0xFFu) | Byte2Marker); - byte byte1 = (byte)(value & 0xFFu); - - // high-bytes to low-bytes - _writer.Write(byte0); - _writer.Write(byte1); - } - else if (value <= (uint.MaxValue >> 2)) - { - byte byte0 = (byte)(((value >> 24) & 0xFFu) | Byte4Marker); - byte byte1 = (byte)((value >> 16) & 0xFFu); - byte byte2 = (byte)((value >> 8) & 0xFFu); - byte byte3 = (byte)(value & 0xFFu); - - // high-bytes to low-bytes - _writer.Write(byte0); - _writer.Write(byte1); - _writer.Write(byte2); - _writer.Write(byte3); - } - else - { - throw new ArgumentException(Resources.Value_too_large_to_be_represented_as_a_30_bit_unsigned_integer); - } - } - - private unsafe void WriteStringValue(string value) - { - if (value == null) - { - _writer.Write((byte)EncodingKind.Null); - } - else - { - if (_stringReferenceMap.TryGetReferenceId(value, out int id)) - { - Debug.Assert(id >= 0); - if (id <= byte.MaxValue) - { - _writer.Write((byte)EncodingKind.StringRef_1Byte); - _writer.Write((byte)id); - } - else if (id <= ushort.MaxValue) - { - _writer.Write((byte)EncodingKind.StringRef_2Bytes); - _writer.Write((ushort)id); - } - else - { - _writer.Write((byte)EncodingKind.StringRef_4Bytes); - _writer.Write(id); - } - } - else - { - _stringReferenceMap.Add(value, isReusable: true); - - if (value.IsValidUnicodeString()) - { - // Usual case - the string can be encoded as UTF8: - // We can use the UTF8 encoding of the binary writer. - - _writer.Write((byte)EncodingKind.StringUtf8); - _writer.Write(value); - } - else - { - _writer.Write((byte)EncodingKind.StringUtf16); - - // This is rare, just allocate UTF16 bytes for simplicity. - byte[] bytes = new byte[(uint)value.Length * sizeof(char)]; - fixed (char* valuePtr = value) - { - Marshal.Copy((IntPtr)valuePtr, bytes, 0, bytes.Length); - } - - WriteCompressedUInt((uint)value.Length); - _writer.Write(bytes); - } - } - } - } - - private void WriteArray(Array array) - { - int length = array.GetLength(0); - - switch (length) - { - case 0: - _writer.Write((byte)EncodingKind.Array_0); - break; - case 1: - _writer.Write((byte)EncodingKind.Array_1); - break; - case 2: - _writer.Write((byte)EncodingKind.Array_2); - break; - case 3: - _writer.Write((byte)EncodingKind.Array_3); - break; - default: - _writer.Write((byte)EncodingKind.Array); - this.WriteCompressedUInt((uint)length); - break; - } - - var elementType = array.GetType().GetElementType(); - - if (s_typeMap.TryGetValue(elementType, out var elementKind)) - { - this.WritePrimitiveType(elementType, elementKind); - this.WritePrimitiveTypeArrayElements(elementType, elementKind, array); - } - else - { - // emit header up front - this.WriteKnownType(elementType); - - // recursive: write elements now - var oldDepth = _recursionDepth; - _recursionDepth++; - - if (_recursionDepth % MaxRecursionDepth == 0) - { - // If we're recursing too deep, move the work to another thread to do so we - // don't blow the stack. 'LongRunning' ensures that we get a dedicated thread - // to do this work. That way we don't end up blocking the threadpool. - var task = Task.Factory.StartNew( - a => WriteArrayValues((Array)a), - array, - _cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - // We must not proceed until the additional task completes. After returning from a write, the underlying - // stream providing access to raw memory will be closed; if this occurs before the separate thread - // completes its write then an access violation can occur attempting to write to unmapped memory. - // - // CANCELLATION: If cancellation is required, DO NOT attempt to cancel the operation by cancelling this - // wait. Cancellation must only be implemented by modifying 'task' to cancel itself in a timely manner - // so the wait can complete. - task.GetAwaiter().GetResult(); - } - else - { - WriteArrayValues(array); - } - - _recursionDepth--; - Debug.Assert(_recursionDepth == oldDepth); - } - } - - private void WriteArrayValues(Array array) - { - for (int i = 0; i < array.Length; i++) - { - this.WriteValue(array.GetValue(i)); - } - } - - private void WritePrimitiveTypeArrayElements(Type type, EncodingKind kind, Array instance) - { - Debug.Assert(s_typeMap[type] == kind); - - // optimization for type underlying binary writer knows about - if (type == typeof(byte)) - { - _writer.Write((byte[])instance); - } - else if (type == typeof(char)) - { - _writer.Write((char[])instance); - } - else if (type == typeof(string)) - { - // optimization for string which object writer has - // its own optimization to reduce repeated string - WriteStringArrayElements((string[])instance); - } - else if (type == typeof(bool)) - { - // optimization for bool array - WriteBooleanArrayElements((bool[])instance); - } - else - { - // otherwise, write elements directly to underlying binary writer - switch (kind) - { - case EncodingKind.Int8: - WriteInt8ArrayElements((sbyte[])instance); - return; - case EncodingKind.Int16: - WriteInt16ArrayElements((short[])instance); - return; - case EncodingKind.Int32: - WriteInt32ArrayElements((int[])instance); - return; - case EncodingKind.Int64: - WriteInt64ArrayElements((long[])instance); - return; - case EncodingKind.UInt16: - WriteUInt16ArrayElements((ushort[])instance); - return; - case EncodingKind.UInt32: - WriteUInt32ArrayElements((uint[])instance); - return; - case EncodingKind.UInt64: - WriteUInt64ArrayElements((ulong[])instance); - return; - case EncodingKind.Float4: - WriteFloat4ArrayElements((float[])instance); - return; - case EncodingKind.Float8: - WriteFloat8ArrayElements((double[])instance); - return; - case EncodingKind.Decimal: - WriteDecimalArrayElements((decimal[])instance); - return; - default: - throw ExceptionUtilities.UnexpectedValue(kind); - } - } - } - - private void WriteBooleanArrayElements(bool[] array) - { - // convert bool array to bit array - var bits = BitVector.Create(array.Length); - for (var i = 0; i < array.Length; i++) - { - bits[i] = array[i]; - } - - // send over bit array - foreach (var word in bits.Words()) - { - _writer.Write(word); - } - } - - private void WriteStringArrayElements(string[] array) - { - for (var i = 0; i < array.Length; i++) - { - WriteStringValue(array[i]); - } - } - - private void WriteInt8ArrayElements(sbyte[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteInt16ArrayElements(short[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteInt32ArrayElements(int[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteInt64ArrayElements(long[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteUInt16ArrayElements(ushort[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteUInt32ArrayElements(uint[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteUInt64ArrayElements(ulong[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteDecimalArrayElements(decimal[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteFloat4ArrayElements(float[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WriteFloat8ArrayElements(double[] array) - { - for (var i = 0; i < array.Length; i++) - { - _writer.Write(array[i]); - } - } - - private void WritePrimitiveType(Type type, EncodingKind kind) - { - Debug.Assert(s_typeMap[type] == kind); - _writer.Write((byte)kind); - } - - public void WriteType(Type type) - { - _writer.Write((byte)EncodingKind.Type); - this.WriteString(type.AssemblyQualifiedName); - } - - private void WriteKnownType(Type type) - { - _writer.Write((byte)EncodingKind.Type); - this.WriteInt32(_binderSnapshot.GetTypeId(type)); - } - - private void WriteObject(object instance, IObjectWritable instanceAsWritableOpt) - { - Debug.Assert(instance != null); - Debug.Assert(instanceAsWritableOpt == null || instance == instanceAsWritableOpt); - - _cancellationToken.ThrowIfCancellationRequested(); - - // write object ref if we already know this instance - if (_objectReferenceMap.TryGetReferenceId(instance, out var id)) - { - Debug.Assert(id >= 0); - if (id <= byte.MaxValue) - { - _writer.Write((byte)EncodingKind.ObjectRef_1Byte); - _writer.Write((byte)id); - } - else if (id <= ushort.MaxValue) - { - _writer.Write((byte)EncodingKind.ObjectRef_2Bytes); - _writer.Write((ushort)id); - } - else - { - _writer.Write((byte)EncodingKind.ObjectRef_4Bytes); - _writer.Write(id); - } - } - else - { - var writable = instanceAsWritableOpt; - if (writable == null) - { - writable = instance as IObjectWritable; - if (writable == null) - { - throw NoSerializationWriterException($"{instance.GetType()} must implement {nameof(IObjectWritable)}"); - } - } - - var oldDepth = _recursionDepth; - _recursionDepth++; - - if (_recursionDepth % MaxRecursionDepth == 0) - { - // If we're recursing too deep, move the work to another thread to do so we - // don't blow the stack. 'LongRunning' ensures that we get a dedicated thread - // to do this work. That way we don't end up blocking the threadpool. - var task = Task.Factory.StartNew( - obj => WriteObjectWorker((IObjectWritable)obj), - writable, - _cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - // We must not proceed until the additional task completes. After returning from a write, the underlying - // stream providing access to raw memory will be closed; if this occurs before the separate thread - // completes its write then an access violation can occur attempting to write to unmapped memory. - // - // CANCELLATION: If cancellation is required, DO NOT attempt to cancel the operation by cancelling this - // wait. Cancellation must only be implemented by modifying 'task' to cancel itself in a timely manner - // so the wait can complete. - task.GetAwaiter().GetResult(); - } - else - { - WriteObjectWorker(writable); - } - - _recursionDepth--; - Debug.Assert(_recursionDepth == oldDepth); - } - } - - private void WriteObjectWorker(IObjectWritable writable) - { - _objectReferenceMap.Add(writable, writable.ShouldReuseInSerialization); - - // emit object header up front - _writer.Write((byte)EncodingKind.Object); - - // Directly write out the type-id for this object. i.e. no need to write out the 'Type' - // tag since we just wrote out the 'Object' tag - this.WriteInt32(_binderSnapshot.GetTypeId(writable.GetType())); - writable.WriteTo(this); - } - - private static Exception NoSerializationTypeException(string typeName) - { - return new InvalidOperationException(string.Format(Resources.The_type_0_is_not_understood_by_the_serialization_binder, typeName)); - } - - private static Exception NoSerializationWriterException(string typeName) - { - return new InvalidOperationException(string.Format(Resources.Cannot_serialize_type_0, typeName)); - } - - // we have s_typeMap and s_reversedTypeMap since there is no bidirectional map in compiler - // Note: s_typeMap is effectively immutable. However, for maximum perf we use mutable types because - // they are used in hotspots. - internal static readonly Dictionary s_typeMap; - - /// - /// Indexed by EncodingKind. - /// - internal static readonly ImmutableArray s_reverseTypeMap; - - static ObjectWriter() - { - s_typeMap = new Dictionary - { - { typeof(bool), EncodingKind.BooleanType }, - { typeof(char), EncodingKind.Char }, - { typeof(string), EncodingKind.StringType }, - { typeof(sbyte), EncodingKind.Int8 }, - { typeof(short), EncodingKind.Int16 }, - { typeof(int), EncodingKind.Int32 }, - { typeof(long), EncodingKind.Int64 }, - { typeof(byte), EncodingKind.UInt8 }, - { typeof(ushort), EncodingKind.UInt16 }, - { typeof(uint), EncodingKind.UInt32 }, - { typeof(ulong), EncodingKind.UInt64 }, - { typeof(float), EncodingKind.Float4 }, - { typeof(double), EncodingKind.Float8 }, - { typeof(decimal), EncodingKind.Decimal }, - }; - - var temp = new Type[(int)EncodingKind.Last]; - - foreach (var kvp in s_typeMap) - { - temp[(int)kvp.Value] = kvp.Key; - } - - s_reverseTypeMap = ImmutableArray.Create(temp); - } - - /// - /// byte marker mask for encoding compressed uint - /// - internal const byte ByteMarkerMask = 3 << 6; - - /// - /// byte marker bits for uint encoded in 1 byte. - /// - internal const byte Byte1Marker = 0; - - /// - /// byte marker bits for uint encoded in 2 bytes. - /// - internal const byte Byte2Marker = 1 << 6; - - /// - /// byte marker bits for uint encoded in 4 bytes. - /// - internal const byte Byte4Marker = 2 << 6; - - internal enum EncodingKind : byte - { - /// - /// The null value - /// - Null, - - /// - /// A type - /// - Type, - - /// - /// An object with member values encoded as variants - /// - Object, - - /// - /// An object reference with the id encoded as 1 byte. - /// - ObjectRef_1Byte, - - /// - /// An object reference with the id encode as 2 bytes. - /// - ObjectRef_2Bytes, - - /// - /// An object reference with the id encoded as 4 bytes. - /// - ObjectRef_4Bytes, - - /// - /// A string encoded as UTF8 (using BinaryWriter.Write(string)) - /// - StringUtf8, - - /// - /// A string encoded as UTF16 (as array of UInt16 values) - /// - StringUtf16, - - /// - /// A reference to a string with the id encoded as 1 byte. - /// - StringRef_1Byte, - - /// - /// A reference to a string with the id encoded as 2 bytes. - /// - StringRef_2Bytes, - - /// - /// A reference to a string with the id encoded as 4 bytes. - /// - StringRef_4Bytes, - - /// - /// The boolean value true. - /// - Boolean_True, - - /// - /// The boolean value char. - /// - Boolean_False, - - /// - /// A character value encoded as 2 bytes. - /// - Char, - - /// - /// An Int8 value encoded as 1 byte. - /// - Int8, - - /// - /// An Int16 value encoded as 2 bytes. - /// - Int16, - - /// - /// An Int32 value encoded as 4 bytes. - /// - Int32, - - /// - /// An Int32 value encoded as 1 byte. - /// - Int32_1Byte, - - /// - /// An Int32 value encoded as 2 bytes. - /// - Int32_2Bytes, - - /// - /// The Int32 value 0 - /// - Int32_0, - - /// - /// The Int32 value 1 - /// - Int32_1, - - /// - /// The Int32 value 2 - /// - Int32_2, - - /// - /// The Int32 value 3 - /// - Int32_3, - - /// - /// The Int32 value 4 - /// - Int32_4, - - /// - /// The Int32 value 5 - /// - Int32_5, - - /// - /// The Int32 value 6 - /// - Int32_6, - - /// - /// The Int32 value 7 - /// - Int32_7, - - /// - /// The Int32 value 8 - /// - Int32_8, - - /// - /// The Int32 value 9 - /// - Int32_9, - - /// - /// The Int32 value 10 - /// - Int32_10, - - /// - /// An Int64 value encoded as 8 bytes - /// - Int64, - - /// - /// A UInt8 value encoded as 1 byte. - /// - UInt8, - - /// - /// A UIn16 value encoded as 2 bytes. - /// - UInt16, - - /// - /// A UInt32 value encoded as 4 bytes. - /// - UInt32, - - /// - /// A UInt32 value encoded as 1 byte. - /// - UInt32_1Byte, - - /// - /// A UInt32 value encoded as 2 bytes. - /// - UInt32_2Bytes, - - /// - /// The UInt32 value 0 - /// - UInt32_0, - - /// - /// The UInt32 value 1 - /// - UInt32_1, - - /// - /// The UInt32 value 2 - /// - UInt32_2, - - /// - /// The UInt32 value 3 - /// - UInt32_3, - - /// - /// The UInt32 value 4 - /// - UInt32_4, - - /// - /// The UInt32 value 5 - /// - UInt32_5, - - /// - /// The UInt32 value 6 - /// - UInt32_6, - - /// - /// The UInt32 value 7 - /// - UInt32_7, - - /// - /// The UInt32 value 8 - /// - UInt32_8, - - /// - /// The UInt32 value 9 - /// - UInt32_9, - - /// - /// The UInt32 value 10 - /// - UInt32_10, - - /// - /// A UInt64 value encoded as 8 bytes. - /// - UInt64, - - /// - /// A float value encoded as 4 bytes. - /// - Float4, - - /// - /// A double value encoded as 8 bytes. - /// - Float8, - - /// - /// A decimal value encoded as 12 bytes. - /// - Decimal, - - /// - /// A DateTime value - /// - DateTime, - - /// - /// An array with length encoded as compressed uint - /// - Array, - - /// - /// An array with zero elements - /// - Array_0, - - /// - /// An array with one element - /// - Array_1, - - /// - /// An array with 2 elements - /// - Array_2, - - /// - /// An array with 3 elements - /// - Array_3, - - /// - /// The boolean type - /// - BooleanType, - - /// - /// The string type - /// - StringType, - - - Last = StringType + 1, - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/PooledObject.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/PooledObject.cs deleted file mode 100644 index 6dcd91c5..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/PooledObject.cs +++ /dev/null @@ -1,168 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using System.Text; -using Microsoft.CodeAnalysis.PooledObjects; - -namespace Microsoft.CodeAnalysis -{ - /// - /// this is RAII object to automatically release pooled object when its owning pool - /// - internal struct PooledObject : IDisposable where T : class - { - private readonly Action, T> _releaser; - private readonly ObjectPool _pool; - private T _pooledObject; - - public PooledObject(ObjectPool pool, Func, T> allocator, Action, T> releaser) : this() - { - _pool = pool; - _pooledObject = allocator(pool); - _releaser = releaser; - } - - public T Object => _pooledObject; - - public void Dispose() - { - if (_pooledObject != null) - { - _releaser(_pool, _pooledObject); - _pooledObject = null; - } - } - - #region factory - public static PooledObject Create(ObjectPool pool) - { - return new PooledObject( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject Create(ObjectPool pool) - { - return new PooledObject( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject> Create(ObjectPool> pool) - { - return new PooledObject>( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject> Create(ObjectPool> pool) - { - return new PooledObject>( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject> Create(ObjectPool> pool) - { - return new PooledObject>( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject> Create(ObjectPool> pool) - { - return new PooledObject>( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - - public static PooledObject> Create(ObjectPool> pool) - { - return new PooledObject>( - pool, - p => Allocator(p), - (p, sb) => Releaser(p, sb)); - } - #endregion - - #region allocators and releasers - private static StringBuilder Allocator(ObjectPool pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool pool, StringBuilder sb) - { - pool.ClearAndFree(sb); - } - - private static Stopwatch Allocator(ObjectPool pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool pool, Stopwatch sb) - { - pool.ClearAndFree(sb); - } - - private static Stack Allocator(ObjectPool> pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool> pool, Stack obj) - { - pool.ClearAndFree(obj); - } - - private static Queue Allocator(ObjectPool> pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool> pool, Queue obj) - { - pool.ClearAndFree(obj); - } - - private static HashSet Allocator(ObjectPool> pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool> pool, HashSet obj) - { - pool.ClearAndFree(obj); - } - - private static Dictionary Allocator(ObjectPool> pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool> pool, Dictionary obj) - { - pool.ClearAndFree(obj); - } - - private static List Allocator(ObjectPool> pool) - { - return pool.AllocateAndClear(); - } - - private static void Releaser(ObjectPool> pool, List obj) - { - pool.ClearAndFree(obj); - } - #endregion - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ReferenceEqualityComparer.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ReferenceEqualityComparer.cs deleted file mode 100644 index e41b8668..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/ReferenceEqualityComparer.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Roslyn.Utilities -{ - /// - /// Compares objects based upon their reference identity. - /// - internal class ReferenceEqualityComparer : IEqualityComparer - { - public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); - - private ReferenceEqualityComparer() - { - } - - bool IEqualityComparer.Equals(object a, object b) - { - return a == b; - } - - int IEqualityComparer.GetHashCode(object a) - { - return ReferenceEqualityComparer.GetHashCode(a); - } - - public static int GetHashCode(object a) - { - return RuntimeHelpers.GetHashCode(a); - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/RoslynStubs.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/RoslynStubs.cs deleted file mode 100644 index 9ff0d7fc..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/RoslynStubs.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.CodeAnalysis.Internal.Log -{ - - static class FunctionId - { - public static readonly string SpellChecker_ExceptionInCacheRead = nameof (SpellChecker_ExceptionInCacheRead); - - public static string BKTree_ExceptionInCacheRead { get; internal set; } - } -} - -namespace Roslyn.Utilities -{ - static class ExceptionUtilities - { - public static Exception UnexpectedValue (ObjectWriter.EncodingKind kind) => throw new Exception ($"Unexpected value of kind {kind}"); - public static Exception UnexpectedValue (Type type) => throw new Exception ($"Unexpected value of type {type}"); - - public static Exception UnexpectedValue (byte marker) - { - throw new Exception ($"Unexpected value for marker {marker}"); - } - } - - static class WorkspacesResources - { - public static string Deserialization_reader_for_0_read_incorrect_number_of_values - => "Deserialization reader for {0} read incorrect number of values"; - public static string The_type_0_is_not_understood_by_the_serialization_binder - => "The type {0} is not understood by the serialization binder"; - public static string Cannot_serialize_type_0 - => "Cannot serialize type {0}"; - - public static string Arrays_with_more_than_one_dimension_cannot_be_serialized { get; internal set; } - public static string Value_too_large_to_be_represented_as_a_30_bit_unsigned_integer { get; internal set; } - } - - internal static partial class SpecializedCollections - { - public static IList EmptyList () - { - return Empty.List.Instance; - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPoolExtensions.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPoolExtensions.cs deleted file mode 100644 index 4738a6dc..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPoolExtensions.cs +++ /dev/null @@ -1,227 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics; -using System.Text; -using Microsoft.CodeAnalysis.PooledObjects; - -namespace Microsoft.CodeAnalysis -{ - internal static class SharedPoolExtensions - { - private const int Threshold = 512; - - public static PooledObject GetPooledObject(this ObjectPool pool) - { - return PooledObject.Create(pool); - } - - public static PooledObject GetPooledObject(this ObjectPool pool) - { - return PooledObject.Create(pool); - } - - public static PooledObject> GetPooledObject(this ObjectPool> pool) - { - return PooledObject>.Create(pool); - } - - public static PooledObject> GetPooledObject(this ObjectPool> pool) - { - return PooledObject>.Create(pool); - } - - public static PooledObject> GetPooledObject(this ObjectPool> pool) - { - return PooledObject>.Create(pool); - } - - public static PooledObject> GetPooledObject(this ObjectPool> pool) - { - return PooledObject>.Create(pool); - } - - public static PooledObject> GetPooledObject(this ObjectPool> pool) - { - return PooledObject>.Create(pool); - } - - public static PooledObject GetPooledObject(this ObjectPool pool) where T : class - { - return new PooledObject(pool, p => p.Allocate(), (p, o) => p.Free(o)); - } - - public static StringBuilder AllocateAndClear(this ObjectPool pool) - { - var sb = pool.Allocate(); - sb.Clear(); - - return sb; - } - - public static Stopwatch AllocateAndClear(this ObjectPool pool) - { - var watch = pool.Allocate(); - watch.Reset(); - - return watch; - } - - public static Stack AllocateAndClear(this ObjectPool> pool) - { - var set = pool.Allocate(); - set.Clear(); - - return set; - } - - public static Queue AllocateAndClear(this ObjectPool> pool) - { - var set = pool.Allocate(); - set.Clear(); - - return set; - } - - public static HashSet AllocateAndClear(this ObjectPool> pool) - { - var set = pool.Allocate(); - set.Clear(); - - return set; - } - - public static Dictionary AllocateAndClear(this ObjectPool> pool) - { - var map = pool.Allocate(); - map.Clear(); - - return map; - } - - public static List AllocateAndClear(this ObjectPool> pool) - { - var list = pool.Allocate(); - list.Clear(); - - return list; - } - - public static void ClearAndFree(this ObjectPool pool, StringBuilder sb) - { - if (sb == null) - { - return; - } - - sb.Clear(); - - if (sb.Capacity > Threshold) - { - sb.Capacity = Threshold; - } - - pool.Free(sb); - } - - public static void ClearAndFree(this ObjectPool pool, Stopwatch watch) - { - if (watch == null) - { - return; - } - - watch.Reset(); - pool.Free(watch); - } - - public static void ClearAndFree(this ObjectPool> pool, HashSet set) - { - if (set == null) - { - return; - } - - var count = set.Count; - set.Clear(); - - if (count > Threshold) - { - set.TrimExcess(); - } - - pool.Free(set); - } - - public static void ClearAndFree(this ObjectPool> pool, Stack set) - { - if (set == null) - { - return; - } - - var count = set.Count; - set.Clear(); - - if (count > Threshold) - { - set.TrimExcess(); - } - - pool.Free(set); - } - - public static void ClearAndFree(this ObjectPool> pool, Queue set) - { - if (set == null) - { - return; - } - - var count = set.Count; - set.Clear(); - - if (count > Threshold) - { - set.TrimExcess(); - } - - pool.Free(set); - } - - public static void ClearAndFree(this ObjectPool> pool, Dictionary map) - { - if (map == null) - { - return; - } - - // if map grew too big, don't put it back to pool - if (map.Count > Threshold) - { - pool.ForgetTrackedObject(map); - return; - } - - map.Clear(); - pool.Free(map); - } - - public static void ClearAndFree(this ObjectPool> pool, List list) - { - if (list == null) - { - return; - } - - list.Clear(); - - if (list.Capacity > Threshold) - { - list.Capacity = Threshold; - } - - pool.Free(list); - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPools.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPools.cs deleted file mode 100644 index 478d8e2a..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SharedPools.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Generic; -using Microsoft.CodeAnalysis.PooledObjects; - -namespace Microsoft.CodeAnalysis -{ - /// - /// Shared object pool for roslyn - /// - /// Use this shared pool if only concern is reducing object allocations. - /// if perf of an object pool itself is also a concern, use ObjectPool directly. - /// - /// For example, if you want to create a million of small objects within a second, - /// use the ObjectPool directly. it should have much less overhead than using this. - /// - internal static class SharedPools - { - // byte pooled memory : 4K * 512 = 4MB - public const int ByteBufferSize = 4 * 1024; - private const int ByteBufferCount = 512; - - /// - /// Used to reduce the # of temporary byte[]s created to satisfy serialization and - /// other I/O requests - /// - public static readonly ObjectPool ByteArray = new ObjectPool(() => new byte[ByteBufferSize], ByteBufferCount); - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Collection.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Collection.cs deleted file mode 100644 index 9a98ce49..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Collection.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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.Generic; - -namespace Roslyn.Utilities -{ - internal static partial class SpecializedCollections - { - private static partial class Empty - { - internal class Collection : Enumerable, ICollection - { - public static readonly ICollection Instance = new Collection(); - - protected Collection() - { - } - - public void Add(T item) - { - throw new NotSupportedException(); - } - - public void Clear() - { - throw new NotSupportedException(); - } - - public bool Contains(T item) - { - return false; - } - - public void CopyTo(T[] array, int arrayIndex) - { - } - - public int Count => 0; - - public bool IsReadOnly => true; - - public bool Remove(T item) - { - throw new NotSupportedException(); - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerable.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerable.cs deleted file mode 100644 index cd88c66f..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerable.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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.Collections.Generic; - -namespace Roslyn.Utilities -{ - internal partial class SpecializedCollections - { - private partial class Empty - { - internal class Enumerable : IEnumerable - { - // PERF: cache the instance of enumerator. - // accessing a generic static field is kinda slow from here, - // but since empty enumerables are singletons, there is no harm in having - // one extra instance field - private readonly IEnumerator _enumerator = Enumerator.Instance; - - public IEnumerator GetEnumerator() - { - return _enumerator; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator.cs deleted file mode 100644 index 092e336f..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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; - -namespace Roslyn.Utilities -{ - internal partial class SpecializedCollections - { - private partial class Empty - { - internal class Enumerator : IEnumerator - { - public static readonly IEnumerator Instance = new Enumerator(); - - protected Enumerator() - { - } - - public object Current => throw new InvalidOperationException(); - - public bool MoveNext() - { - return false; - } - - public void Reset() - { - throw new InvalidOperationException(); - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator`1.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator`1.cs deleted file mode 100644 index c4ad9ab6..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.Enumerator`1.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.Generic; - -namespace Roslyn.Utilities -{ - internal partial class SpecializedCollections - { - private partial class Empty - { - internal class Enumerator : Enumerator, IEnumerator - { - public static new readonly IEnumerator Instance = new Enumerator(); - - protected Enumerator() - { - } - - public new T Current => throw new InvalidOperationException(); - - public void Dispose() - { - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.List.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.List.cs deleted file mode 100644 index 9097dc42..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpecializedCollections.Empty.List.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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.Generic; - -namespace Roslyn.Utilities -{ - internal partial class SpecializedCollections - { - private partial class Empty - { - internal class List : Collection, IList, IReadOnlyList - { - public static readonly new List Instance = new List(); - - protected List() - { - } - - public int IndexOf(T item) - { - return -1; - } - - public void Insert(int index, T item) - { - throw new NotSupportedException(); - } - - public void RemoveAt(int index) - { - throw new NotSupportedException(); - } - - public T this[int index] - { - get - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - set - { - throw new NotSupportedException(); - } - } - } - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpellChecker.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpellChecker.cs deleted file mode 100644 index eaaf729c..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/SpellChecker.cs +++ /dev/null @@ -1,256 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Utilities; -using Microsoft.Extensions.Logging; - -namespace Roslyn.Utilities -{ - internal partial class SpellChecker : IObjectWritable, IChecksummedObject - { - private const string SerializationFormat = "3"; - - public Checksum Checksum { get; } - - private readonly BKTree _bkTree; - - public SpellChecker (Checksum checksum, BKTree bKTree) - { - Checksum = checksum; - _bkTree = bKTree; - } - - public SpellChecker (Checksum checksum, IEnumerable corpus) - : this (checksum, BKTree.Create (corpus)) - { - } - - public IList FindSimilarWords (string value) - => FindSimilarWords (value, substringsAreSimilar: false); - - public IList FindSimilarWords (string value, bool substringsAreSimilar) - { - var result = _bkTree.Find (value, threshold: null); - - var checker = WordSimilarityChecker.Allocate (value, substringsAreSimilar); - var array = result.Where (checker.AreSimilar).ToArray (); - checker.Free (); - - return array; - } - - bool IObjectWritable.ShouldReuseInSerialization => true; - - void IObjectWritable.WriteTo (ObjectWriter writer) - { - writer.WriteString (SerializationFormat); - Checksum.WriteTo (writer); - _bkTree.WriteTo (writer); - } - - internal static SpellChecker TryReadFrom (ObjectReader reader, ILogger logger) - { - try { - var formatVersion = reader.ReadString (); - if (string.Equals (formatVersion, SerializationFormat, StringComparison.Ordinal)) { - var checksum = Checksum.ReadFrom (reader); - var bkTree = BKTree.ReadFrom (reader, logger); - if (bkTree != null) { - return new SpellChecker (checksum, bkTree); - } - } - } catch (Exception ex) { - LogExceptionInCacheRead (logger, ex); - } - - return null; - } - - [LoggerMessage (EventId = 0, Level = LogLevel.Error, Message = "Exception in SpellChecker cache read")] - static partial void LogExceptionInCacheRead (ILogger logger, Exception ex); - } - - internal class WordSimilarityChecker - { - private struct CacheResult - { - public readonly string CandidateText; - public readonly bool AreSimilar; - public readonly double SimilarityWeight; - - public CacheResult (string candidate, bool areSimilar, double similarityWeight) - { - CandidateText = candidate; - AreSimilar = areSimilar; - SimilarityWeight = similarityWeight; - } - } - - // Cache the result of the last call to AreSimilar. We'll often be called with the same - // value multiple times in a row, so we can avoid expensive computation by returning the - // same value immediately. - private CacheResult _lastAreSimilarResult; - - private string _source; - private EditDistance _editDistance; - private int _threshold; - - /// - /// Whether or words should be considered similar if one is contained within the other - /// (regardless of edit distance). For example if is true then IService would be considered - /// similar to IServiceFactory despite the edit distance being quite high at 7. - /// - private bool _substringsAreSimilar; - - private static readonly object s_poolGate = new object (); - private static readonly Stack s_pool = new Stack (); - - public static WordSimilarityChecker Allocate (string text, bool substringsAreSimilar) - { - WordSimilarityChecker checker; - lock (s_poolGate) { - checker = s_pool.Count > 0 - ? s_pool.Pop () - : new WordSimilarityChecker (); - } - - checker.Initialize (text, substringsAreSimilar); - return checker; - } - - private WordSimilarityChecker () - { - } - - private void Initialize (string text, bool substringsAreSimilar) - { - _source = text ?? throw new ArgumentNullException (nameof (text)); - _threshold = GetThreshold (_source); - _editDistance = new EditDistance (text); - _substringsAreSimilar = substringsAreSimilar; - } - - public void Free () - { - _editDistance?.Dispose (); - _source = null; - _editDistance = null; - _lastAreSimilarResult = default; - lock (s_poolGate) { - s_pool.Push (this); - } - } - - public static bool AreSimilar (string originalText, string candidateText) - => AreSimilar (originalText, candidateText, substringsAreSimilar: false); - - public static bool AreSimilar (string originalText, string candidateText, bool substringsAreSimilar) - => AreSimilar (originalText, candidateText, substringsAreSimilar, out var unused); - - public static bool AreSimilar (string originalText, string candidateText, out double similarityWeight) - { - return AreSimilar ( - originalText, candidateText, - substringsAreSimilar: false, similarityWeight: out similarityWeight); - } - - /// - /// Returns true if 'originalText' and 'candidateText' are likely a misspelling of each other. - /// Returns false otherwise. If it is a likely misspelling a similarityWeight is provided - /// to help rank the match. Lower costs mean it was a better match. - /// - public static bool AreSimilar (string originalText, string candidateText, bool substringsAreSimilar, out double similarityWeight) - { - var checker = Allocate (originalText, substringsAreSimilar); - var result = checker.AreSimilar (candidateText, out similarityWeight); - checker.Free (); - - return result; - } - - internal static int GetThreshold (string value) - => value.Length <= 4 ? 1 : 2; - - public bool AreSimilar (string candidateText) - => AreSimilar (candidateText, out var similarityWeight); - - public bool AreSimilar (string candidateText, out double similarityWeight) - { - if (_source.Length < 3) { - // If we're comparing strings that are too short, we'll find - // far too many spurious hits. Don't even bother in this case. - similarityWeight = double.MaxValue; - return false; - } - - if (_lastAreSimilarResult.CandidateText == candidateText) { - similarityWeight = _lastAreSimilarResult.SimilarityWeight; - return _lastAreSimilarResult.AreSimilar; - } - - var result = AreSimilarWorker (candidateText, out similarityWeight); - _lastAreSimilarResult = new CacheResult (candidateText, result, similarityWeight); - return result; - } - - private bool AreSimilarWorker (string candidateText, out double similarityWeight) - { - similarityWeight = double.MaxValue; - - // If the two strings differ by more characters than the cost threshold, then there's - // no point in even computing the edit distance as it would necessarily take at least - // that many additions/deletions. - if (Math.Abs (_source.Length - candidateText.Length) <= _threshold) { - similarityWeight = _editDistance.GetEditDistance (candidateText, _threshold); - } - - if (similarityWeight > _threshold) { - // it had a high cost. However, the string the user typed was contained - // in the string we're currently looking at. That's enough to consider it - // although we place it just at the threshold (i.e. it's worse than all - // other matches). - if (_substringsAreSimilar && candidateText.IndexOf (_source, StringComparison.OrdinalIgnoreCase) >= 0) { - similarityWeight = _threshold; - } else { - return false; - } - } - - Debug.Assert (similarityWeight <= _threshold); - - similarityWeight += Penalty (candidateText, _source); - return true; - } - - private static double Penalty (string candidateText, string originalText) - { - var lengthDifference = Math.Abs (originalText.Length - candidateText.Length); - if (lengthDifference != 0) { - // For all items of the same edit cost, we penalize those that are - // much longer than the original text versus those that are only - // a little longer. - // - // Note: even with this penalty, all matches of cost 'X' will all still - // cost less than matches of cost 'X + 1'. i.e. the penalty is in the - // range [0, 1) and only serves to order matches of the same cost. - // - // Here's the relation of the first few values of length diff and penalty: - // LengthDiff -> Penalty - // 1 -> .5 - // 2 -> .66 - // 3 -> .75 - // 4 -> .8 - // And so on and so forth. - var penalty = 1.0 - (1.0 / (lengthDifference + 1)); - return penalty; - } - - return 0; - } - } -} \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringExtensions.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringExtensions.cs deleted file mode 100644 index 275b1915..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace Roslyn.Utilities -{ - internal static class StringExtensions - { - internal static bool IsValidUnicodeString(this string str) - { - int i = 0; - while (i < str.Length) - { - char c = str[i++]; - - // (high surrogate, low surrogate) makes a valid pair, anything else is invalid: - if (char.IsHighSurrogate(c)) - { - if (i < str.Length && char.IsLowSurrogate(str[i])) - { - i++; - } - else - { - // high surrogate not followed by low surrogate - return false; - } - } - else if (char.IsLowSurrogate(c)) - { - // previous character wasn't a high surrogate - return false; - } - } - - return true; - } - } -} diff --git a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringSlice.cs b/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringSlice.cs deleted file mode 100644 index 14cf17de..00000000 --- a/MonoDevelop.MSBuild.Editor/RoslynSpellChecker/StringSlice.cs +++ /dev/null @@ -1,197 +0,0 @@ -// 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.Generic; -using System.Diagnostics; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Utilities -{ - internal struct StringSlice : IEquatable - { - private readonly string _underlyingString; - private readonly TextSpan _span; - - public StringSlice(string underlyingString, TextSpan span) - { - _underlyingString = underlyingString; - _span = span; - - Debug.Assert(span.Start >= 0); - Debug.Assert(span.End <= underlyingString.Length); - } - - public StringSlice(string value) : this(value, new TextSpan(0, value.Length)) - { - } - - public int Length => _span.Length; - - public char this[int index] => _underlyingString[_span.Start + index]; - - public Enumerator GetEnumerator() - { - return new Enumerator(this); - } - - public override bool Equals(object obj) => Equals((StringSlice)obj); - - public bool Equals(StringSlice other) => EqualsOrdinal(other); - - internal bool EqualsOrdinal(StringSlice other) - { - if (this._span.Length != other._span.Length) - { - return false; - } - - var end = this._span.End; - for (int i = this._span.Start, j = other._span.Start; i < end; i++, j++) - { - if (this._underlyingString[i] != other._underlyingString[j]) - { - return false; - } - } - - return true; - } - - internal bool EqualsOrdinalIgnoreCase(StringSlice other) - { - if (this._span.Length != other._span.Length) - { - return false; - } - - var end = this._span.End; - for (int i = this._span.Start, j = other._span.Start; i < end; i++, j++) - { - var thisChar = this._underlyingString[i]; - var otherChar = other._underlyingString[j]; - - if (!EqualsOrdinalIgnoreCase(thisChar, otherChar)) - { - return false; - } - } - - return true; - } - - private static bool EqualsOrdinalIgnoreCase(char thisChar, char otherChar) - { - // Do a fast check first before converting to lowercase characters. - return - thisChar == otherChar || - CaseInsensitiveComparison.ToLower(thisChar) == CaseInsensitiveComparison.ToLower(otherChar); - } - - public override int GetHashCode() => GetHashCodeOrdinal(); - - internal int GetHashCodeOrdinal() - { - return Hash.GetFNVHashCode(this._underlyingString, this._span.Start, this._span.Length); - } - - internal int GetHashCodeOrdinalIgnoreCase() - { - return Hash.GetCaseInsensitiveFNVHashCode(this._underlyingString, this._span.Start, this._span.Length); - } - - internal int CompareToOrdinal(StringSlice other) - { - var thisEnd = this._span.End; - var otherEnd = other._span.End; - for (int i = this._span.Start, j = other._span.Start; - i < thisEnd && j < otherEnd; - i++, j++) - { - var diff = this._underlyingString[i] - other._underlyingString[j]; - if (diff != 0) - { - return diff; - } - } - - // Choose the one that is shorter if their prefixes match so far. - return this.Length - other.Length; - } - - internal int CompareToOrdinalIgnoreCase(StringSlice other) - { - var thisEnd = this._span.End; - var otherEnd = other._span.End; - for (int i = this._span.Start, j = other._span.Start; - i < thisEnd && j < otherEnd; - i++, j++) - { - var diff = - CaseInsensitiveComparison.ToLower(this._underlyingString[i]) - - CaseInsensitiveComparison.ToLower(other._underlyingString[j]); - if (diff != 0) - { - return diff; - } - } - - // Choose the one that is shorter if their prefixes match so far. - return this.Length - other.Length; - } - - public struct Enumerator - { - private readonly StringSlice _stringSlice; - private int index; - - public Enumerator(StringSlice stringSlice) - { - _stringSlice = stringSlice; - index = -1; - } - - public bool MoveNext() - { - index++; - return index < _stringSlice.Length; - } - - public char Current => _stringSlice[index]; - } - } - - internal abstract class StringSliceComparer : IComparer, IEqualityComparer - { - public static readonly StringSliceComparer Ordinal = new OrdinalComparer(); - public static readonly StringSliceComparer OrdinalIgnoreCase = new OrdinalIgnoreCaseComparer(); - - private class OrdinalComparer : StringSliceComparer - { - public override int Compare(StringSlice x, StringSlice y) - => x.CompareToOrdinal(y); - - public override bool Equals(StringSlice x, StringSlice y) - => x.EqualsOrdinal(y); - - public override int GetHashCode(StringSlice obj) - => obj.GetHashCodeOrdinal(); - } - - private class OrdinalIgnoreCaseComparer : StringSliceComparer - { - public override int Compare(StringSlice x, StringSlice y) - => x.CompareToOrdinalIgnoreCase(y); - - public override bool Equals(StringSlice x, StringSlice y) - => x.EqualsOrdinalIgnoreCase(y); - - public override int GetHashCode(StringSlice obj) - => obj.GetHashCodeOrdinalIgnoreCase(); - } - - public abstract int Compare(StringSlice x, StringSlice y); - public abstract bool Equals(StringSlice x, StringSlice y); - public abstract int GetHashCode(StringSlice obj); - } -} diff --git a/MonoDevelop.MSBuild.Tests.Editor/Completion/MSBuildCompletionTests.cs b/MonoDevelop.MSBuild.Tests.Editor/Completion/MSBuildCompletionTests.cs index db4b1f41..a59927bb 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Completion/MSBuildCompletionTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Completion/MSBuildCompletionTests.cs @@ -455,8 +455,16 @@ public async Task PathCompletion () result.AssertNonEmpty (); result.AssertContains ("foo.txt"); + } + + [Test] + public async Task PathCompletionDirectory () + { + var testDirectory = TestMSBuildFileSystem.Instance.AddTestDirectory (); + testDirectory.AddFiles ("foo.txt", "bar.cs", "baz.cs"); + testDirectory.AddDirectory ("foo").AddFiles ("hello.cs", "bye.cs"); - result = await this.GetCompletionContext ( + var result = await this.GetCompletionContext ( @"foo$", CompletionTriggerReason.Insertion, '\\', filename: testDirectory.Combine ("PathCompletion.csproj")); diff --git a/MonoDevelop.MSBuild.Tests.Editor/Completion/TestFileSystem.cs b/MonoDevelop.MSBuild.Tests.Editor/Completion/TestFileSystem.cs index f6767f28..804f27d8 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Completion/TestFileSystem.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Completion/TestFileSystem.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable annotations + using System; using System.Collections.Generic; using System.ComponentModel.Composition; @@ -32,7 +34,7 @@ class TestMSBuildFileSystem : TestDirectoryInfo, IMSBuildFileSystem public IEnumerable GetFiles (string path) => GetDirectory (path) is TestDirectoryInfo info ? info.GetFiles () : Enumerable.Empty (); - public TestDirectoryInfo AddTestDirectory ([CallerMemberName] string testName = null) + public TestDirectoryInfo AddTestDirectory ([CallerMemberName] string? testName = null) { if (string.IsNullOrEmpty (testName)) { throw new ArgumentException ($"'{nameof (testName)}' cannot be null or empty.", nameof (testName)); @@ -47,11 +49,11 @@ class TestDirectoryInfo readonly HashSet files = new (); internal readonly Dictionary Directories = new (); - readonly string name; - readonly TestDirectoryInfo parent; - string path = null; + readonly string? name; + readonly TestDirectoryInfo? parent; + string? path = null; - public TestDirectoryInfo (string name, TestDirectoryInfo parent) + public TestDirectoryInfo (string? name, TestDirectoryInfo? parent) { this.name = name; this.parent = parent; @@ -71,7 +73,7 @@ TestDirectoryInfo RootDir { public string Combine (string name) => $"{Path}/{name}"; - public TestDirectoryInfo GetDirectory (string path) + public TestDirectoryInfo? GetDirectory (string path) { var currentDir = this; bool isFirst = true; @@ -96,7 +98,7 @@ public TestDirectoryInfo AddDirectory (string path) currentDir = RootDir; } else if (!currentDir.Directories.TryGetValue (segment, out var nextDir)) { nextDir = new TestDirectoryInfo (segment, currentDir); - currentDir.Directories.Add (nextDir.name, nextDir); + currentDir.Directories.Add (nextDir.name!, nextDir); currentDir = nextDir; } isFirst = false; diff --git a/MonoDevelop.MSBuild.Tests.Editor/Completion/TestSchemaProvider.cs b/MonoDevelop.MSBuild.Tests.Editor/Completion/TestSchemaProvider.cs index 419d3432..8178f954 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Completion/TestSchemaProvider.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Completion/TestSchemaProvider.cs @@ -1,10 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable annotations + using System; using System.Collections.Generic; using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Composition; + using MonoDevelop.MSBuild.Language.Typesystem; using MonoDevelop.MSBuild.Schema; @@ -13,11 +17,11 @@ namespace MonoDevelop.MSBuild.Tests.Editor.Completion [Export (typeof (MSBuildSchemaProvider))] class TestSchemaProvider : MSBuildSchemaProvider { - public override MSBuildSchema GetSchema (string path, string sdk, out IList loadErrors) + public override MSBuildSchema? GetSchema (string path, string? sdk, out IList? loadErrors) { loadErrors = Array.Empty (); - switch (path) { + switch (System.IO.Path.GetFileName(path)) { case "EagerElementTrigger.csproj": return new MSBuildSchema { new PropertyInfo ("Foo", null, valueKind: MSBuildValueKind.Bool) diff --git a/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs b/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs index 7ea18b17..a86af013 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs @@ -14,6 +14,7 @@ using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests; +using MonoDevelop.Xml.Tests.Utils; using NUnit.Framework; diff --git a/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs b/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs index 569e5a5f..829a11c1 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Editor.Roslyn; using MonoDevelop.MSBuild.Tests.Helpers; using MonoDevelop.Xml.Editor.Tests; @@ -24,6 +25,7 @@ protected override Task OnInitialize () protected override IEnumerable GetAssembliesToCompose () => base.GetAssembliesToCompose ().Concat (new[] { typeof (MSBuildCompletionSource).Assembly.Location, + typeof (TaskMetadataBuilder).Assembly.Location, typeof (MSBuildTestEnvironment).Assembly.Location }); @@ -31,6 +33,6 @@ protected override bool ShouldIgnoreCompositionError (string error) => error.Contains ("Microsoft.VisualStudio.Editor.ICommonEditorAssetServiceFactory") || error.Contains ("MonoDevelop.MSBuild.Editor.Host.IStreamingFindReferencesPresenter") || error.Contains ("Microsoft.VisualStudio.Language.Intellisense.ISuggestedActionCategoryRegistryService2") - ; + || base.ShouldIgnoreCompositionError (error); } } diff --git a/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEditorHost.cs b/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEditorHost.cs index cea68ff1..9259a48a 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEditorHost.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEditorHost.cs @@ -51,19 +51,6 @@ public FindReferencesContext StartSearch (string title, string referenceName, bo => throw new System.NotImplementedException (); } - [Export (typeof (IDifferenceBufferFactoryService))] - class MockDifferenceBufferFactoryService : IDifferenceBufferFactoryService - { - public IDifferenceBuffer CreateDifferenceBuffer (ITextBuffer leftBaseBuffer, ITextBuffer rightBaseBuffer) - { - throw new System.NotImplementedException (); - } - - public IDifferenceBuffer CreateDifferenceBuffer (ITextBuffer leftBaseBuffer, ITextBuffer rightBaseBuffer, StringDifferenceOptions options, bool disableEditing = false, bool wrapLeftBuffer = true, bool wrapRightBuffer = true) => throw new System.NotImplementedException (); - - public IDifferenceBuffer TryGetDifferenceBuffer (IProjectionBufferBase projectionBuffer) => throw new System.NotImplementedException (); - } - [Export (typeof (IDifferenceViewElementFactory))] class MockDifferenceViewElementFactoryService : IDifferenceViewElementFactory { diff --git a/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEnvironment.cs b/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEnvironment.cs index 6a0aa624..5f439627 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEnvironment.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Mocks/TestMSBuildEnvironment.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; -using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Editor; using MonoDevelop.MSBuild.SdkResolution; namespace MonoDevelop.MSBuild.Tests.Editor.Mocks diff --git a/MonoDevelop.MSBuild.Tests.Editor/MonoDevelop.MSBuild.Tests.Editor.csproj b/MonoDevelop.MSBuild.Tests.Editor/MonoDevelop.MSBuild.Tests.Editor.csproj index 09d80043..6bfd4389 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MonoDevelop.MSBuild.Tests.Editor.csproj +++ b/MonoDevelop.MSBuild.Tests.Editor/MonoDevelop.MSBuild.Tests.Editor.csproj @@ -1,8 +1,7 @@ - net8.0 - net48;net8.0 + net48 true @@ -11,7 +10,6 @@ - @@ -20,16 +18,13 @@ + + - - - - - diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs index 1ca26f85..2f5de5ae 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs @@ -3,6 +3,7 @@ #nullable enable + using System; using System.Linq; using System.Threading.Tasks; @@ -10,10 +11,10 @@ using MonoDevelop.MSBuild.Editor.Refactorings.ExtractExpression; using MonoDevelop.MSBuild.Language.Expressions; using MonoDevelop.MSBuild.Language.Syntax; -using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests.Parser; +using MonoDevelop.Xml.Tests.Utils; using NUnit.Framework; using NUnit.Framework.Internal; @@ -81,10 +82,9 @@ public Task ExtractFromTaskToExistingGroupInProject () => TestExtractExpression "); - async Task TestExtractExpression (string textWithMarkers, int expectedFixCount, string invokeFixWithTitle, string expectedTextAfterInvoke, string typeText, string expectedTextAfterTyping) + public Task TestExtractExpression (string textWithMarkers, int expectedFixCount, string invokeFixWithTitle, string expectedTextAfterInvoke, string typeText, string expectedTextAfterTyping) { - var context = await this.GetRefactorings (textWithMarkers); - await this.TestCodeFixContext (context, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping); + return this.TestRefactoring (textWithMarkers, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping); } [Test] diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs index 5a031f4e..b3dd1860 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs @@ -16,9 +16,11 @@ using MonoDevelop.MSBuild.Analysis; using MonoDevelop.MSBuild.Editor.Analysis; using MonoDevelop.MSBuild.Schema; -using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Editor.Tests.Extensions; using MonoDevelop.Xml.Tests; +using MonoDevelop.Xml.Tests.Utils; + +using TextSpan = MonoDevelop.Xml.Dom.TextSpan; using NUnit.Framework; @@ -45,20 +47,21 @@ await refactoringService.GetRefactorings (parseResult, selection, cancellationTo ); } - public static Task GetRefactorings (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker = '|', CancellationToken cancellationToken = default) + public static async Task GetRefactorings (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker = '|', CancellationToken cancellationToken = default) where T : MSBuildRefactoringProvider, new() { var refactoringService = new MSBuildRefactoringService (new[] { new T () }); - var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker); + var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker, allowZeroWidthSingleMarker: true); - return test.GetRefactorings (refactoringService, textView, cancellationToken); + return await test.GetRefactorings (refactoringService, textView, cancellationToken); } - public static ITextView CreateTextViewWithSelection (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker) + public static ITextView CreateTextViewWithSelection (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker, bool allowZeroWidthSingleMarker = false) { var parsed = TextWithMarkers.Parse (documentWithSelection, selectionMarker); + var text = parsed.Text; - var selection = parsed.GetMarkedSpan (selectionMarker); + TextSpan selection = parsed.GetMarkedSpan (selectionMarker, allowZeroWidthSingleMarker); var textView = test.CreateTextView (text); @@ -77,7 +80,7 @@ public static ITextView CreateTextViewWithCaret (this MSBuildEditorTest test, st var position = parsed.GetMarkedPosition (caretMarker); var textView = test.CreateTextView (text); - + return textView; } @@ -93,6 +96,7 @@ public static async Task TestRefactoring ( CancellationToken cancellationToken = default ) where T : MSBuildRefactoringProvider, new() { + await test.Catalog.JoinableTaskContext.Factory.SwitchToMainThreadAsync (); var ctx = await test.GetRefactorings (documentWithSelection, selectionMarker, cancellationToken); await test.TestCodeFixContext(ctx, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping, cancellationToken); } @@ -146,7 +150,7 @@ public static Task GetCodeFixes ( where TAnalyzer : MSBuildAnalyzer, new() where TCodeFix : MSBuildFixProvider, new() { - var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker); + var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker, allowZeroWidthSingleMarker: true); return test.GetCodeFixes ([new TAnalyzer ()], [new TCodeFix ()], textView, textView.Selection.SelectedSpans.Single(), requestedSeverities, false, null, logger, cancellationToken); } @@ -165,6 +169,7 @@ public static async Task TestCodeFix ( where TAnalyzer : MSBuildAnalyzer, new() where TCodeFix : MSBuildFixProvider, new() { + await test.Catalog.JoinableTaskContext.Factory.SwitchToMainThreadAsync (); var ctx = await test.GetCodeFixes (documentWithSelection, selectionMarker: selectionMarker, cancellationToken: cancellationToken); await test.TestCodeFixContext (ctx, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping, cancellationToken); } @@ -180,6 +185,10 @@ public static async Task TestCodeFixContext ( CancellationToken cancellationToken = default ) { + if (!test.Catalog.JoinableTaskContext.IsOnMainThread) { + throw new InvalidOperationException ("Must be on main thread"); + } + Assert.That (ctx.CodeFixes, Has.Count.EqualTo (expectedFixCount)); Assert.That (ctx.CodeFixes.Select (c => c.Action.Title), Has.One.EqualTo (invokeFixWithTitle)); diff --git a/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs b/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs index 6ed6d527..81e78023 100644 --- a/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs +++ b/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs @@ -15,6 +15,7 @@ using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests; +using MonoDevelop.Xml.Tests.Utils; using NUnit.Framework; @@ -76,8 +77,13 @@ protected IEnumerable GetExpressionCompletion ( // based on MSBuildCompletionSource.{GetExpressionCompletionsAsync,GetAdditionalCompletionsAsync} // eventually we can factor out into a shared method - string expression = GetIncompleteValue (spineParser, textSource); - int exprStartPos = caretPos - expression.Length; + // TryGetIncompleteValue may return false while still outputting incomplete values, if it fails due to reaching maximum readahead. + // It will also return false and output null values if we're in an element value that only contains whitespace. + // In both these cases we can ignore the false return and proceed anyways. + spineParser.TryGetIncompleteValue (textSource, out var expression, out var valueSpan); + expression ??= ""; + int exprStartPos = valueSpan?.Start ?? caretPos; + var triggerState = ExpressionCompletion.GetTriggerState (expression, caretPos - exprStartPos, reason, triggerChar, rr.IsCondition (), out int spanStart, out int spanLength, out ExpressionNode triggerExpression, out var listKind, out IReadOnlyList comparandVariables, logger @@ -111,45 +117,6 @@ protected IEnumerable GetExpressionCompletion ( return ExpressionCompletion.GetCompletionInfos (rr, triggerState, valueSymbol, triggerExpression, spanLength, parsedDocument, functionTypeProvider, fileSystem, logger); } - // copied from XmlParserSnapshotExtensions, modified to use ITextSource instead of ITextSnapshot - static string GetIncompleteValue (XmlSpineParser spineAtCaret, ITextSource textSource) - { - int caretPosition = spineAtCaret.Position; - var node = spineAtCaret.Spine.Peek (); - - int valueStart; - if (node is XText t) { - valueStart = t.Span.Start; - } else if (node is XElement el && el.IsEnded) { - valueStart = el.Span.End; - } else { - int lineStart = GetLineStart (textSource, caretPosition); - valueStart = spineAtCaret.Position - spineAtCaret.CurrentStateLength; - if (spineAtCaret.GetAttributeValueDelimiter ().HasValue) { - valueStart += 1; - } - valueStart = Math.Min (Math.Max (valueStart, lineStart), caretPosition); - } - - return textSource.GetText (valueStart, caretPosition - valueStart); - - static int GetLineStart (ITextSource textSource, int caretPosition) - { - if (caretPosition < 1) { - return caretPosition; - } - int lineStart = caretPosition - 1; - for (; lineStart >= 0; lineStart--) { - switch (textSource[caretPosition]) { - case '\r': - case '\n': - return lineStart + 1; - } - } - return lineStart; - } - } - class TestFunctionTypeProvider : IFunctionTypeProvider { public Task EnsureInitialized (CancellationToken token) => Task.CompletedTask; diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs index afecabee..326c9c36 100644 --- a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs @@ -108,7 +108,7 @@ public static void VerifyDiagnostics ( Is.EquivalentTo (expectedDiag.Properties ?? Enumerable.Empty> ()) .UsingDictionaryComparer ()); // checks messageArgs - Assert.That (actualDiag.GetFormattedMessage (), Is.EqualTo (expectedDiag.GetFormattedMessage ())); + Assert.That (actualDiag.GetFormattedMessageWithTitle (), Is.EqualTo (expectedDiag.GetFormattedMessageWithTitle ())); found = true; actualDiagnostics.RemoveAt (i); break; diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs index d58d9104..a28b3ea3 100644 --- a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs @@ -5,11 +5,11 @@ using System.Collections.Generic; using MonoDevelop.MSBuild.Language; -using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests; +using MonoDevelop.Xml.Tests.Utils; namespace MonoDevelop.MSBuild.Tests; diff --git a/MonoDevelop.MSBuild.Tests/MonoDevelop.MSBuild.Tests.csproj b/MonoDevelop.MSBuild.Tests/MonoDevelop.MSBuild.Tests.csproj index 241e8f70..bd3935be 100644 --- a/MonoDevelop.MSBuild.Tests/MonoDevelop.MSBuild.Tests.csproj +++ b/MonoDevelop.MSBuild.Tests/MonoDevelop.MSBuild.Tests.csproj @@ -13,6 +13,8 @@ + + @@ -20,4 +22,8 @@ + + + + diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/.editorconfig b/MonoDevelop.MSBuild.Tests/PackageSearch/.editorconfig new file mode 100644 index 00000000..1fe8c7a3 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: http://EditorConfig.org + +[*.cs] + +# revert settings to match roslyn style better +indent_style = space +trim_trailing_whitespace = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_keywords_in_control_flow_statements = false + +# Newline settings +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false + +# VS threading analyzer triggers on imported roslyn code +dotnet_diagnostic.VSTHRD002.severity = none +dotnet_diagnostic.VSTHRD003.severity = none +dotnet_diagnostic.VSTHRD103.severity = none +dotnet_diagnostic.VSTHRD110.severity = none \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockFileSystem.cs b/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockFileSystem.cs new file mode 100644 index 00000000..910a152b --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockFileSystem.cs @@ -0,0 +1,47 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.NuGetSearch.Tests; + +public class MockFileSystem : IFileSystem +{ + public bool DirectoryExists(string path) + { + throw new NotImplementedException(); + } + + public IEnumerable EnumerateDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + throw new NotImplementedException(); + } + + public IEnumerable EnumerateFiles(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + throw new NotImplementedException(); + } + + public bool FileExists(string path) + { + throw new NotImplementedException(); + } + + public string GetDirectoryName(string path) + { + throw new NotImplementedException(); + } + + public string GetDirectoryNameOnly(string path) + { + throw new NotImplementedException(); + } + + public string ReadAllText(string path) + { + throw new NotImplementedException(); + } +} diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockWebRequestFactory.cs b/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockWebRequestFactory.cs new file mode 100644 index 00000000..2d7a3827 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/Mocks/MockWebRequestFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.NuGetSearch.Tests; + +public class MockWebRequestFactory : IWebRequestFactory +{ + public Task GetStringAsync(string endpoint, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/NuGetV2ServiceFeedTests.cs b/MonoDevelop.MSBuild.Tests/PackageSearch/NuGetV2ServiceFeedTests.cs new file mode 100644 index 00000000..13573a18 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/NuGetV2ServiceFeedTests.cs @@ -0,0 +1,111 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using FluentAssertions; + +using Moq; + +using NUnit.Framework; + +using ProjectFileTools.NuGetSearch.Feeds; +using ProjectFileTools.NuGetSearch.Feeds.Web; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.NuGetSearch.Tests; + +/// +/// The tests below only work when signing is disabled. +/// When signing enabled, no test will be found as a result of `ProjectFileTools.NuGetSearch` failing to load with signing key validation error +/// +[TestFixture] +public class NuGetV2ServiceFeedTests +{ + + [Theory] + [TestCase("http://localhost/nuget")] + public void GivenFeed_ReturnDisplayName(string feed) + { + var webRequestFactory = Mock.Of(); + var sut = new NuGetV2ServiceFeed(feed, webRequestFactory); + sut.DisplayName.Should().Be($"{feed} (NuGet v2)"); + } + + [Theory] + [TestCase("http://localhost/nuget", "GetPackageNames.CommonLogging.xml")] + public async Task GivenPackagesFound_ReturnListOfIds(string feed, string testFile) + { + var webRequestFactory = Mock.Of(); + + Mock.Get(webRequestFactory) + .Setup(f => f.GetStringAsync("http://localhost/nuget/Search()?searchTerm='Common.Logging'&targetFramework=netcoreapp2.0&includePrerelease=False&semVerLevel=2.0.0", It.IsAny())) + .Returns(Task.FromResult(GetXmlFromTestFile(testFile))); + + var sut = new NuGetV2ServiceFeed(feed, webRequestFactory); + + var packageNameResults = await sut.GetPackageNamesAsync("Common.Logging", new PackageQueryConfiguration("netcoreapp2.0", false), new CancellationToken()); + packageNameResults.Names.Count.Should().Be(5); + } + + [Theory] + [TestCase("http://localhost/nuget", "GetPackageVersions.CommonLogging.xml")] + public async Task GivenPackagesFound_ReturnListOfVersions(string feed, string testFile) + { + var webRequestFactory = Mock.Of(); + + Mock.Get(webRequestFactory) + .Setup(f => f.GetStringAsync("http://localhost/nuget/FindPackagesById()?id='Acme.Common.Logging.AspNetCore'", It.IsAny())) + .Returns(Task.FromResult(GetXmlFromTestFile(testFile))); + + var sut = new NuGetV2ServiceFeed(feed, webRequestFactory); + + var packageNameResults = await sut.GetPackageVersionsAsync("Acme.Common.Logging.AspNetCore", new PackageQueryConfiguration("netcoreapp2.0", false), new CancellationToken()); + packageNameResults.Versions.Count.Should().Be(8); + Assert.That(packageNameResults.Versions, Is.EqualTo (new[] { + "1.6.0.5", + "1.6.1", + "1.6.2", + "1.7.0", + "1.7.1", + "1.8.0", + "1.9.0", + "1.9.1" + })); + } + + [Theory] + [TestCase("http://localhost/nuget", "GetPackageInfo.CommonLogging.xml")] + public async Task GivenPackageFound_ReturnPackageInfo(string feed, string testFile) + { + var webRequestFactory = Mock.Of(); + + Mock.Get(webRequestFactory) + .Setup(f => f.GetStringAsync("http://localhost/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.8.0')", It.IsAny())) + .Returns(Task.FromResult(GetXmlFromTestFile(testFile))); + + var sut = new NuGetV2ServiceFeed(feed, webRequestFactory); + + var pkgInfo = await sut.GetPackageInfoAsync("Acme.Common.Logging.AspNetCore", "1.8.0", new PackageQueryConfiguration("netcoreapp2.0", false), new CancellationToken()); + + pkgInfo.Id.Should().Be("Acme.Common.Logging.AspNetCore"); + pkgInfo.Title.Should().Be("Common Logging AspNetCore"); + pkgInfo.Summary.Should().BeNullOrEmpty(); + pkgInfo.Description.Should().Be("Common Logging integration within Aspnet core services"); + pkgInfo.Authors.Should().Be("Patrick Assuied"); + pkgInfo.Version.Should().Be("1.8.0"); + pkgInfo.ProjectUrl.Should().Be("https://bitbucket.acme.com/projects/Acme/repos/Acme-common-logging"); + pkgInfo.LicenseUrl.Should().BeNullOrEmpty(); + pkgInfo.Tags.Should().Be(" common logging aspnetcore "); + } + + public static string GetXmlFromTestFile(string filename, [CallerFilePath] string callerFile = null) + { + var asmDir = Path.GetDirectoryName(typeof(NuGetV2ServiceFeedTests).Assembly.Location); + string path = Path.Combine(asmDir, "PackageSearch", "TestFiles", filename); + return File.ReadAllText(path); + } +} diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageInfo.CommonLogging.xml b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageInfo.CommonLogging.xml new file mode 100644 index 00000000..a74c6a4a --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageInfo.CommonLogging.xml @@ -0,0 +1,47 @@ + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.8.0') + + + + Acme.Common.Logging.AspNetCore + 2018-11-21T17:31:07Z + 2018-11-21T17:31:07Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.8.0 + 1.8.0 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2018-11-21T17:31:07.1944763Z + 2018-11-21T17:31:07.1944763Z + Acme.Common.Logging.Configuration:1.8.0:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.3:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.0|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.0|Acme.Common.Logging.Configuration:1.8.0:netcoreapp2.1|Microsoft.AspNetCore.Diagnostics:2.1.1:netcoreapp2.1|NLog.Web.AspNetCore:4.5.4:netcoreapp2.1|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.1|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.1 + KP5xJayLLtWJx59wz6rSHW8tvxNY4xp/3eCsqG2QF7wINphpXs+0k2rKLigMWq/6geWptbdXXjJdZXYJUG0EUQ== + SHA512 + 22002 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageNames.CommonLogging.xml b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageNames.CommonLogging.xml new file mode 100644 index 00000000..71370d1b --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageNames.CommonLogging.xml @@ -0,0 +1,753 @@ + + + http://schemas.datacontract.org/2004/07/ + + <updated>2019-07-07T04:48:53Z</updated> + <link rel="self" href="http://localhost/Nuget/nuget/Packages" /> + <entry> + <id>http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')</id> + <category term="NuGet.Server.Core.DataServices.ODataPackage" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> + <link rel="edit" href="http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')" /> + <link rel="self" href="http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')" /> + <title type="text">Acme.Common.Logging.AspNetCore + 2018-04-09T19:12:53Z + 2018-04-09T19:12:53Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.6.0.5 + 1.6.0.5 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + + ## 1.6.0 + - Support for .NET core + + 2018-04-09T19:12:53.6418356Z + 2018-04-09T19:12:53.6418356Z + Acme.Common.Logging.Configuration:1.6.0.5:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.1:netcoreapp2.0 + zclx15xLnnUxm63+IqBWUNCYWAYcHvumBD1CbsXuSpdame0fHPcfv9FlMRodIOHUF9q/yXqkGo/AQTRlMAa88Q== + SHA512 + 11395 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.1') + + + + Acme.Common.Logging.AspNetCore + 2018-05-04T21:45:54Z + 2018-05-04T21:45:54Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.6.1 + 1.6.1 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + + ## 1.6.0 + - Support for .NET core + + 2018-05-04T21:45:54.0222438Z + 2018-05-04T21:45:54.0222438Z + Acme.Common.Logging.Configuration:1.6.1:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.1:netcoreapp2.0 + 7uBobOiTDhyz6s4P1Fnzgh1PokLYXU7G5E7nHDIpeCoj1B/f/augtWBBcqpuxv+hyDnGsVtyOxxPqso1Jhk1yw== + SHA512 + 11399 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Configuration',Version='1.1.2.0') + + + + Acme.Common.Logging.Configuration + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Configuration + 1.1.2.0 + 1.1.2 + false + Acme Common Logging Configuration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + https://wiki.Acmeondemand.com/display/PLAT/Acme.Common.Logging + -1 + false + false + Common Logging Configuration + + + 2016-11-16T04:09:55.6754695Z + 2016-11-16T04:09:55.6754695Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3 + Cu8qd2sESppuG4hEc3Etlum0S82vhTmDDaLqKHwMfGWuz57sGZkiQyARLEvIDL96eAb4szOslpAYxAhUApuapQ== + SHA512 + 16428 + Copyright 2016 + common logging configuration + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Configuration',Version='1.1.3.0') + + + + Acme.Common.Logging.Configuration + 2016-12-12T18:26:29Z + 2016-12-12T18:26:29Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Configuration + 1.1.3.0 + 1.1.3 + false + Acme Common Logging Configuration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + https://wiki.Acmeondemand.com/display/PLAT/Acme.Common.Logging + -1 + false + false + Common Logging Configuration + + + 2016-12-12T18:26:29.8353212Z + 2016-12-12T18:26:29.8353212Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3 + +Jtb8P8swXBxC8HBqViEyp/0N3+fwoMCPbcTWHeUPOnA93/5P7EopkShQ0XudRK2cMa1LiZf/9AW7HFkNstpqg== + SHA512 + 17319 + Copyright 2016 + common logging configuration + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Configuration',Version='1.1.3.2') + + + + Acme.Common.Logging.Configuration + 2016-12-29T06:29:44Z + 2016-12-29T06:29:44Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Configuration + 1.1.3.2 + 1.1.3.2 + false + Acme Common Logging Configuration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + https://wiki.Acmeondemand.com/display/PLAT/Acme.Common.Logging + -1 + false + false + Common Logging Configuration + + + 2016-12-29T06:29:44.4691432Z + 2016-12-29T06:29:44.4701478Z + Common.Logging.NLog41:3.3.1|Acme.Configuration:1.0.1.0|Microsoft.AspNet.WebApi.Core:5.2.3 + N0r+Kg3T294d7uvAP5THOTZcPpuGD30GEev1HGuWXj5eqGH+APRFGvMppfOvyYTcjJfh8HJM8t2hvfXEnJbe8w== + SHA512 + 38607 + Copyright 2016 + common logging configuration + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Context.Autofac',Version='1.0.0.2') + + + + Acme.Common.Logging.Context.Autofac + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Context.Autofac + 1.0.0.2 + 1.0.0.2 + false + Acme Common Logging Platform Context Autofac integration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration with Platform Context using Autofac + + + 2016-11-16T04:09:55.6874701Z + 2016-11-16T04:09:55.6874701Z + Autofac:3.5.2|Common.Logging.NLog41:3.3.1|Acme.Platform.Context.Serialization:1.0.3.0|Acme.Common.Logging.Configuration:1.0.0.2 + xBSkrpuRVaN4VE6jG1bzrJduetp/ErZm0M5/jEdoAIERLdwN4coAJw2AC5YITxgYUFiVea2RnIBfHk17Qjs6AA== + SHA512 + 9482 + Copyright 2016 + common logging platform context autofac + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Context.Autofac',Version='1.0.1.0') + + + + Acme.Common.Logging.Context.Autofac + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Context.Autofac + 1.0.1.0 + 1.0.1 + false + Acme Common Logging Platform Context Autofac integration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration with Platform Context using Autofac + + + 2016-11-16T04:09:55.6984711Z + 2016-11-16T04:09:55.6984711Z + Autofac:3.5.2|Common.Logging.NLog41:3.3.1|Acme.Platform.Context.Serialization:1.0.3.0|Acme.Common.Logging.Configuration:1.0.1.0 + Q2UiaZF1NLAiRA/pBDZmjock5Rmf1dnE0FkLft8T+/Vkkp1GczNTOYLy99xOJ4XDnk3/oHPYK97N4vfJXA3i2g== + SHA512 + 10537 + Copyright 2016 + common logging platform context autofac + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.Context.Autofac',Version='1.0.2.0') + + + + Acme.Common.Logging.Context.Autofac + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.Context.Autofac + 1.0.2.0 + 1.0.2 + false + Acme Common Logging Platform Context Autofac integration + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration with Platform Context using Autofac + + + 2016-11-16T04:09:55.7144706Z + 2016-11-16T04:09:55.7154707Z + Autofac:3.5.2|Common.Logging.NLog41:3.3.1|Acme.Platform.Context.Serialization:1.0.3.0|Acme.Common.Logging.Configuration:1.0.2.0 + 7Y/XPZFOSTXFciSKUZfhVGPm/Y6of8+yUVdOLGBRBvUd5dSvbwlhHc8tuMNR/HKczTdeYLtrBalUezv06kyzAQ== + SHA512 + 10540 + Copyright 2016 + common logging platform context autofac + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.NLog',Version='4.5.0') + + + + Acme.Common.Logging.NLog + 2018-04-09T19:13:01Z + 2018-04-09T19:13:01Z + + Patrick Assuied + + + + + Acme.Common.Logging.NLog + 4.5.0 + 4.5.0 + false + Acme Common Logging NLog integration for .NET Framework and .NET Standard + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging NLog Integration for Acme for .NET Framework and .NET Standard. Port of Common.Logging.NLog until proper .NET standard support is added + + + ## 4.5.0 + - Integration with NLog 4.5 + + 2018-04-09T19:13:01.2458675Z + 2018-04-09T19:13:01.2468683Z + Common.Logging:[3.4.1, 4.0.0):net452|NLog:4.5.2:net452|Common.Logging:[3.4.1, 4.0.0):netstandard2.0|NLog:4.5.2:netstandard2.0 + eMynRZ/yRL/I//UEC9Rhp/YE4pOVekFIPH9r39lYuwS0VTlDjKhRH7zyk+A6jhiRRGTQiNORaXaR8NepTix7fg== + SHA512 + 29293 + Copyright 2016-2018 + common logging configuration nlog + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.NLog',Version='4.5.1') + + + + Acme.Common.Logging.NLog + 2018-06-30T18:10:12Z + 2018-06-30T18:10:12Z + + Patrick Assuied + + + + + Acme.Common.Logging.NLog + 4.5.1 + 4.5.1 + false + Acme Common Logging NLog integration for .NET Framework and .NET Standard + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging NLog Integration for Acme for .NET Framework and .NET Standard. Port of Common.Logging.NLog until proper .NET standard support is added + + + 2018-06-30T18:10:12.0216293Z + 2018-06-30T18:10:12.0216293Z + Common.Logging:[3.4.1, 4.0.0):net452|NLog:4.5.6:net452|Common.Logging:[3.4.1, 4.0.0):netstandard2.0|NLog:4.5.6:netstandard2.0 + P7uWjyK1uH48ftn9oI8pSFxfU6e0yzsPDOswYay6wk+q5XTh2mMUjKdNWPy7kee2PrD+8aBLjogQtsEQ8To3Sg== + SHA512 + 29268 + Copyright 2016-2018 + common logging configuration nlog + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.NLog',Version='4.5.2') + + + + Acme.Common.Logging.NLog + 2018-10-25T00:56:28Z + 2018-10-25T00:56:28Z + + Patrick Assuied + + + + + Acme.Common.Logging.NLog + 4.5.2 + 4.5.2 + false + Acme Common Logging NLog integration for .NET Framework and .NET Standard + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging NLog Integration for Acme for .NET Framework and .NET Standard. Port of Common.Logging.NLog until proper .NET standard support is added + + + 2018-10-25T00:56:28.0828448Z + 2018-10-25T00:56:28.0838453Z + Common.Logging:[3.4.1, 4.0.0):net452|NLog:4.5.6:net452|SourceLink.Copy.PdbFiles:2.8.3:net452|SourceLink.Create.CommandLine:2.8.3:net452|Common.Logging:[3.4.1, 4.0.0):netstandard2.0|NLog:4.5.6:netstandard2.0|SourceLink.Copy.PdbFiles:2.8.3:netstandard2.0|SourceLink.Create.CommandLine:2.8.3:netstandard2.0 + zcl4j9wMhszI5WmrqxKYJ43as6ZXxfsK+BNmGCm4A60xMH/t6IFVecOBVTUeBnsDx9tJdQGskFzJuSJUU8E9NA== + SHA512 + 29768 + Copyright 2016-2018 + common logging configuration nlog + true + true + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.WebApi',Version='1.0.0.2') + + + + Acme.Common.Logging.WebApi + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.WebApi + 1.0.0.2 + 1.0.0.2 + false + Acme Common Logging WebApi + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration within Web API Client + + + 2016-11-16T04:09:55.8324708Z + 2016-11-16T04:09:55.8324708Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3|Microsoft.Owin:3.0.1|Acme.Common.Logging.Configuration:1.0.0.2 + RSZgxyO1i61wl3ZFraJotbWHZ+5LLo/CMeo78inmxW+bKo6EqKZIJq4oLW9g4ucPMngjzy6TPTb7mFcwrEXxuw== + SHA512 + 10576 + Copyright 2016 + configuration contracts + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.WebApi',Version='1.0.1.0') + + + + Acme.Common.Logging.WebApi + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.WebApi + 1.0.1.0 + 1.0.1 + false + Acme Common Logging WebApi + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration within Web API Client + + + 2016-11-16T04:09:55.8425001Z + 2016-11-16T04:09:55.8425001Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3|Microsoft.Owin:3.0.1|Acme.Common.Logging.Configuration:1.0.1.0 + 0T7Nc9XQvY41Hjhs37kjavEuco0sCBrQP8hNIMVEvkAdnajMjLWueJNKGzRKaUpzTzyFCWfx5/ON4QJAolBHzg== + SHA512 + 11047 + Copyright 2016 + configuration contracts + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.WebApi',Version='1.0.2.0') + + + + Acme.Common.Logging.WebApi + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.WebApi + 1.0.2.0 + 1.0.2 + false + Acme Common Logging WebApi + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration within Web API Client + + + 2016-11-16T04:09:55.8514712Z + 2016-11-16T04:09:55.8514712Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3|Microsoft.Owin:3.0.1|Acme.Common.Logging.Configuration:1.0.2.0 + 1NXCMeB/UpKQuFKoilR2bIzYuovOQjaB/der3AgkcO5zbLo/bknGt61Cz/CLztUksOG95A9fO1zegI3NtlTmcA== + SHA512 + 11095 + Copyright 2016 + configuration contracts + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.WebApi',Version='1.0.3.0') + + + + Acme.Common.Logging.WebApi + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.WebApi + 1.0.3.0 + 1.0.3 + false + Acme Common Logging WebApi + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + + -1 + false + false + Common Logging integration within Web API Client + + + 2016-11-16T04:09:55.8634725Z + 2016-11-16T04:09:55.8634725Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3|Microsoft.Owin:3.0.1|Acme.Common.Logging.Configuration:1.0.3.0 + X9qk+cNFTpaeyM8SewBHHhbTkZPTsue5JmFIzHXvnwM3UgDWCx3wwa1Q83ALzYdJkwuKKPQMWKmGwdsGcUvarw== + SHA512 + 11107 + Copyright 2016 + configuration contracts + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.WebApi',Version='1.0.3.1') + + + + Acme.Common.Logging.WebApi + 2016-11-16T04:09:55Z + 2016-11-16T04:09:55Z + + Acme OnDemand, Inc. + + + + + Acme.Common.Logging.WebApi + 1.0.3.1 + 1.0.3.1 + false + Acme Common Logging WebApi + Acme OnDemand, Inc. + Acme OnDemand, Inc. + + + https://wiki.Acmeondemand.com/display/PLAT/Acme.Common.Logging + -1 + false + false + Common Logging integration within Web API Client + + + 2016-11-16T04:09:55.8715191Z + 2016-11-16T04:09:55.8715191Z + Common.Logging.NLog41:3.3.1|Microsoft.AspNet.WebApi.Core:5.2.3|Microsoft.Owin:3.0.1|Acme.Common.Logging.Configuration:1.0.3.1 + cKlVcH53vwWV9B2Z71b8BwAcpFEKdA/MSTx7YhLBl71cLJeCu7ODmlDqhWzYLtthZheimN5pv6g1LWfRoiinrA== + SHA512 + 11125 + Copyright 2016 + configuration contracts + false + false + true + -1 + + + + + + \ No newline at end of file diff --git a/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageVersions.CommonLogging.xml b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageVersions.CommonLogging.xml new file mode 100644 index 00000000..039097fe --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/PackageSearch/TestFiles/GetPackageVersions.CommonLogging.xml @@ -0,0 +1,384 @@ + + + http://schemas.datacontract.org/2004/07/ + + <updated>2019-07-07T01:17:34Z</updated> + <link rel="self" href="http://localhost/Nuget/nuget/Packages" /> + <entry> + <id>http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')</id> + <category term="NuGet.Server.Core.DataServices.ODataPackage" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> + <link rel="edit" href="http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')" /> + <link rel="self" href="http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.0.5')" /> + <title type="text">Acme.Common.Logging.AspNetCore + 2018-04-09T19:12:53Z + 2018-04-09T19:12:53Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.6.0.5 + 1.6.0.5 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + + ## 1.6.0 + - Support for .NET core + + 2018-04-09T19:12:53.6418356Z + 2018-04-09T19:12:53.6418356Z + Acme.Common.Logging.Configuration:1.6.0.5:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.1:netcoreapp2.0 + zclx15xLnnUxm63+IqBWUNCYWAYcHvumBD1CbsXuSpdame0fHPcfv9FlMRodIOHUF9q/yXqkGo/AQTRlMAa88Q== + SHA512 + 11395 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.1') + + + + Acme.Common.Logging.AspNetCore + 2018-05-04T21:45:54Z + 2018-05-04T21:45:54Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.6.1 + 1.6.1 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + + ## 1.6.0 + - Support for .NET core + + 2018-05-04T21:45:54.0222438Z + 2018-05-04T21:45:54.0222438Z + Acme.Common.Logging.Configuration:1.6.1:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.1:netcoreapp2.0 + 7uBobOiTDhyz6s4P1Fnzgh1PokLYXU7G5E7nHDIpeCoj1B/f/augtWBBcqpuxv+hyDnGsVtyOxxPqso1Jhk1yw== + SHA512 + 11399 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.6.2') + + + + Acme.Common.Logging.AspNetCore + 2018-05-08T01:00:16Z + 2018-05-08T01:00:16Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.6.2 + 1.6.2 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + + ## 1.6.0 + - Support for .NET core + + 2018-05-08T01:00:16.2897639Z + 2018-05-08T01:00:16.2897639Z + Acme.Common.Logging.Configuration:1.6.2:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.1:netcoreapp2.0 + 5ReG+O25Ok1PxC7FXqbubma1UL3PJx7z8YmsThXUGhOih+2IP3k5fjtQo1pOHuTGc5UjOomhOKaeWEIbUXfshA== + SHA512 + 11399 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.7.0') + + + + Acme.Common.Logging.AspNetCore + 2018-06-30T18:10:04Z + 2018-06-30T18:10:04Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.7.0 + 1.7.0 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2018-06-30T18:10:04.5956766Z + 2018-06-30T18:10:04.5956766Z + Acme.Common.Logging.Configuration:1.7.0:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.2:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0 + GF3yaZUYgqCqTLtBxsVzhthbQxBMWj9wWUOgyqcLuty+YmTaIGGToTPiP7T15oQoV9ti7QTYvKoQGJaJEA1TLg== + SHA512 + 11404 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.7.1') + + + + Acme.Common.Logging.AspNetCore + 2018-10-25T00:56:19Z + 2018-10-25T00:56:19Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.7.1 + 1.7.1 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2018-10-25T00:56:19.9908454Z + 2018-10-25T00:56:19.9908454Z + Acme.Common.Logging.Configuration:1.7.1:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.3:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.0|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.0|Acme.Common.Logging.Configuration:1.7.1:netcoreapp2.1|Microsoft.AspNetCore.Diagnostics:2.1.1:netcoreapp2.1|NLog.Web.AspNetCore:4.5.4:netcoreapp2.1|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.1|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.1 + E2a85BBdwDwqda2OdKClf/8g28csDkeffMPpe/36IFLZwvOspZUMXCb/cQOk5X3EVlls5nHgA1OKEvud1Spz0g== + SHA512 + 21295 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.8.0') + + + + Acme.Common.Logging.AspNetCore + 2018-11-21T17:31:07Z + 2018-11-21T17:31:07Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.8.0 + 1.8.0 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2018-11-21T17:31:07.1944763Z + 2018-11-21T17:31:07.1944763Z + Acme.Common.Logging.Configuration:1.8.0:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.3:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.0|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.0|Acme.Common.Logging.Configuration:1.8.0:netcoreapp2.1|Microsoft.AspNetCore.Diagnostics:2.1.1:netcoreapp2.1|NLog.Web.AspNetCore:4.5.4:netcoreapp2.1|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.1|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.1 + KP5xJayLLtWJx59wz6rSHW8tvxNY4xp/3eCsqG2QF7wINphpXs+0k2rKLigMWq/6geWptbdXXjJdZXYJUG0EUQ== + SHA512 + 22002 + Copyright 2016-2018 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.9.0') + + + + Acme.Common.Logging.AspNetCore + 2019-06-19T00:57:53Z + 2019-06-19T00:57:53Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.9.0 + 1.9.0 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2019-06-19T00:57:53.7070732Z + 2019-06-19T00:57:53.7080737Z + Acme.Common.Logging.Configuration:1.9.0:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.3:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.0|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.0|Acme.Common.Logging.Configuration:1.9.0:netcoreapp2.1|Microsoft.AspNetCore.Diagnostics:2.1.1:netcoreapp2.1|NLog.Web.AspNetCore:4.5.4:netcoreapp2.1|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.1|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.1 + tjgU5FxQZnzaW8oi3MIAPd0Oua7st8fsv1rtKmy3DipZMAZX8tYCrz/W94fq99CgQTD3BN8z6M6174645ZtIHg== + SHA512 + 22007 + Copyright 2016-2019 + common logging aspnetcore + false + false + true + -1 + + + + + + http://localhost/Nuget/nuget/Packages(Id='Acme.Common.Logging.AspNetCore',Version='1.9.1') + + + + Acme.Common.Logging.AspNetCore + 2019-06-20T23:33:38Z + 2019-06-20T23:33:38Z + + Patrick Assuied + + + + + Acme.Common.Logging.AspNetCore + 1.9.1 + 1.9.1 + false + Common Logging AspNetCore + Patrick Assuied + Patrick Assuied + + + https://bitbucket.Acme.com/projects/Acme/repos/Acme-common-logging + -1 + false + false + Common Logging integration within Aspnet core services + + ## Available in CHANGELOG.md + 2019-06-20T23:33:38.3885724Z + 2019-06-20T23:33:38.3885724Z + Acme.Common.Logging.Configuration:1.9.1:netcoreapp2.0|Microsoft.AspNetCore.Diagnostics:2.0.3:netcoreapp2.0|NLog.Web.AspNetCore:4.5.4:netcoreapp2.0|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.0|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.0|Acme.Common.Logging.Configuration:1.9.1:netcoreapp2.1|Microsoft.AspNetCore.Diagnostics:2.1.1:netcoreapp2.1|NLog.Web.AspNetCore:4.5.4:netcoreapp2.1|SourceLink.Copy.PdbFiles:2.8.3:netcoreapp2.1|SourceLink.Create.CommandLine:2.8.3:netcoreapp2.1 + yBIXe3PCTRziev9L+AzWKPszTzv7RGhzxmPs6HWqFfQJUNo/WaAE4ORim7dhiFpjluD0knZV9/dLYKl94n3N5A== + SHA512 + 22013 + Copyright 2016-2019 + common logging aspnetcore + true + true + true + -1 + + + + + \ No newline at end of file diff --git a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnostic.cs b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnostic.cs index ffb511a6..12d605c9 100644 --- a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnostic.cs +++ b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnostic.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; +#nullable enable + using System.Collections.Immutable; using MonoDevelop.Xml.Dom; @@ -13,13 +14,13 @@ public class MSBuildDiagnostic public ImmutableDictionary Properties { get; } public TextSpan Span { get; } - readonly object [] messageArgs; + readonly object[]? messageArgs; - public MSBuildDiagnostic (MSBuildDiagnosticDescriptor descriptor, TextSpan span, ImmutableDictionary properties = null, object[] messageArgs = null) + public MSBuildDiagnostic (MSBuildDiagnosticDescriptor descriptor, TextSpan span, ImmutableDictionary? properties = null, object[]? messageArgs = null) { Descriptor = descriptor; Span = span; - Properties = properties; + Properties = properties ?? ImmutableDictionary.Empty; this.messageArgs = messageArgs; } @@ -28,7 +29,6 @@ public MSBuildDiagnostic (MSBuildDiagnosticDescriptor descriptor, TextSpan span, { } - public string GetFormattedTitle () => Descriptor.GetFormattedTitle (messageArgs); - public string GetFormattedMessage () => Descriptor.GetFormattedMessage (messageArgs); + public string GetFormattedMessageWithTitle () => Descriptor.GetFormattedMessageAndTitle (messageArgs); } } \ No newline at end of file diff --git a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticDescriptor.cs b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticDescriptor.cs index d0d578be..bfeac04d 100644 --- a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticDescriptor.cs +++ b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticDescriptor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable using System; using System.Diagnostics.CodeAnalysis; @@ -13,21 +14,34 @@ public class MSBuildDiagnosticDescriptor public string Id { get; } [StringSyntax (StringSyntaxAttribute.CompositeFormat)] - public string Message { get; } + public string? MessageFormat { get; } public MSBuildDiagnosticSeverity Severity { get; } - public MSBuildDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string message, MSBuildDiagnosticSeverity severity) + public MSBuildDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? messageFormat, MSBuildDiagnosticSeverity severity) { Title = title ?? throw new ArgumentNullException (nameof (title)); Id = id ?? throw new ArgumentNullException (nameof (id)); - Message = message; + MessageFormat = messageFormat; Severity = severity; } public MSBuildDiagnosticDescriptor (string id, string title, MSBuildDiagnosticSeverity severity) : this (id, title, null, severity) { } - internal string GetFormattedTitle (object[] args) => (args?.Length > 0)? string.Format (Title, args) : Title; - internal string GetFormattedMessage (object[] args) => (args?.Length > 0 && Message is string msg) ? string.Format (msg, args) : Message; + internal string GetFormattedMessageAndTitle (object[]? messageArgs) + { + try { + string? message = messageArgs != null && messageArgs.Length > 0 && MessageFormat is string format + ? string.Format (MessageFormat, messageArgs) + : MessageFormat; + return string.IsNullOrEmpty (message) + ? Title + : Title + Environment.NewLine + message; + } catch (FormatException ex) { + // this is likely to be called from somewhere other than where the diagnostic was constructed + // so ensure the error has enough info to track it down + throw new FormatException ($"Error formatting message for diagnostic {Id}", ex); + } + } } } \ No newline at end of file diff --git a/MonoDevelop.MSBuild/Evaluation/Imported/ExceptionHandling.cs b/MonoDevelop.MSBuild/Evaluation/Imported/ExceptionHandling.cs index 0aac16b1..e094b935 100644 --- a/MonoDevelop.MSBuild/Evaluation/Imported/ExceptionHandling.cs +++ b/MonoDevelop.MSBuild/Evaluation/Imported/ExceptionHandling.cs @@ -8,7 +8,7 @@ // // URL: https://raw.githubusercontent.com/dotnet/msbuild/7434b575d12157ef98aeaad3b86c8f235f551c41/src/Shared/ExceptionHandling.cs // -// CHANGES: None +// CHANGES: Commented out XmlSyntaxException check @@ -164,7 +164,8 @@ internal static bool IsIoRelatedException(Exception e) internal static bool IsXmlException(Exception e) { return e is XmlException - || e is XmlSyntaxException +// MODIFICATION +// || e is XmlSyntaxException || e is XmlSchemaException || e is UriFormatException; // XmlTextReader for example uses this under the covers } diff --git a/MonoDevelop.MSBuild/Evaluation/Imported/Stubs.cs b/MonoDevelop.MSBuild/Evaluation/Imported/Stubs.cs index 4223e49d..613e4742 100644 --- a/MonoDevelop.MSBuild/Evaluation/Imported/Stubs.cs +++ b/MonoDevelop.MSBuild/Evaluation/Imported/Stubs.cs @@ -13,14 +13,6 @@ using MonoDevelop.MSBuild; -namespace Microsoft.NET.StringTools -{ - internal static class Strings - { - internal static string WeakIntern (string v) => v; - } -} - namespace Microsoft.Build.Shared.FileSystem { internal static class FileSystems diff --git a/MonoDevelop.MSBuild/ITaskMetadataBuilder.cs b/MonoDevelop.MSBuild/ITaskMetadataBuilder.cs index 4db35465..ebc7722b 100644 --- a/MonoDevelop.MSBuild/ITaskMetadataBuilder.cs +++ b/MonoDevelop.MSBuild/ITaskMetadataBuilder.cs @@ -15,7 +15,7 @@ interface ITaskMetadataBuilder { TaskInfo? CreateTaskInfo ( string typeName, string assemblyName, ExpressionNode assemblyFile, string assemblyFileStr, - string declaredInFile, int declaredAtOffset, + string declaredInFile, Xml.Dom.TextSpan? declarationSpan, IMSBuildEvaluationContext evaluationContext, ILogger logger); } diff --git a/MonoDevelop.MSBuild/InternalsVisibleTo.cs b/MonoDevelop.MSBuild/InternalsVisibleTo.cs index 367e358b..29f9abd5 100644 --- a/MonoDevelop.MSBuild/InternalsVisibleTo.cs +++ b/MonoDevelop.MSBuild/InternalsVisibleTo.cs @@ -7,8 +7,11 @@ [assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Tests, {IVT.PublicKeyAtt}")] [assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Tests.Editor, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Editor.Common, {IVT.PublicKeyAtt}")] [assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Editor, {IVT.PublicKeyAtt}")] [assembly: InternalsVisibleTo ($"XsdSchemaImporter, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MSBuildLanguageServer, {IVT.PublicKeyAtt}")] +[assembly: InternalsVisibleTo ($"MSBuildLanguageServer.Tests, {IVT.PublicKeyAtt}")] [assembly: InternalsVisibleTo ($"MonoDevelop.MSBuildEditor, {IVT.PublicKeyAtt}")] [assembly: InternalsVisibleTo ($"MonoDevelop.MSBuild.Editor.VisualStudio, {IVT.PublicKeyAtt}")] diff --git a/MonoDevelop.MSBuild/Language/CoreDiagnostics.cs b/MonoDevelop.MSBuild/Language/CoreDiagnostics.cs index 77dd880d..8478c9cd 100644 --- a/MonoDevelop.MSBuild/Language/CoreDiagnostics.cs +++ b/MonoDevelop.MSBuild/Language/CoreDiagnostics.cs @@ -406,14 +406,14 @@ class CoreDiagnostics public const string UnknownValue_Id = nameof(UnknownValue); public static readonly MSBuildDiagnosticDescriptor UnknownValue = new ( UnknownValue_Id, - "{1} has unknown value", + "Unknown value", "{0} `{1}` has unknown value `{2}`", MSBuildDiagnosticSeverity.Error); public const string HasDefaultValue_Id = nameof(HasDefaultValue); public static readonly MSBuildDiagnosticDescriptor HasDefaultValue = new ( HasDefaultValue_Id, - "{1} has default value", + "Redundant value", "{0} `{1}` has default value `{2}`", MSBuildDiagnosticSeverity.Warning); diff --git a/MonoDevelop.MSBuild/Language/IFunctionTypeProvider.cs b/MonoDevelop.MSBuild/Language/IFunctionTypeProvider.cs index e656d6f9..7309f28f 100644 --- a/MonoDevelop.MSBuild/Language/IFunctionTypeProvider.cs +++ b/MonoDevelop.MSBuild/Language/IFunctionTypeProvider.cs @@ -2,6 +2,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -17,13 +19,13 @@ interface IFunctionTypeProvider MSBuildValueKind ResolveType (ExpressionPropertyNode node); IEnumerable GetItemFunctionNameCompletions (); IEnumerable GetClassNameCompletions (); - FunctionInfo GetStaticPropertyFunctionInfo (string className, string name); - FunctionInfo GetPropertyFunctionInfo (MSBuildValueKind valueKind, string name); - FunctionInfo GetItemFunctionInfo (string name); - ClassInfo GetClassInfo (string name); + FunctionInfo? GetStaticPropertyFunctionInfo (string className, string name); + FunctionInfo? GetPropertyFunctionInfo (MSBuildValueKind valueKind, string name); + FunctionInfo? GetItemFunctionInfo (string name); + ClassInfo? GetClassInfo (string name); //FIXME: this is super broken and needs completely rethinking - ISymbol GetEnumInfo (string reference); + ISymbol? GetEnumInfo (string reference); Task EnsureInitialized (CancellationToken token); } } diff --git a/MonoDevelop.MSBuild/Language/MSBuildDocumentValidator.cs b/MonoDevelop.MSBuild/Language/MSBuildDocumentValidator.cs index 3cec36ba..9b47a9b0 100644 --- a/MonoDevelop.MSBuild/Language/MSBuildDocumentValidator.cs +++ b/MonoDevelop.MSBuild/Language/MSBuildDocumentValidator.cs @@ -136,7 +136,7 @@ TextSpan[] GetNameSpans (XElement el) => (el.ClosingTag is XClosingTag ct) break; case MSBuildSyntaxKind.Task: - ValidateTaskParameters (element, elementSyntax, (MSBuildTaskElement) elementSymbol); + ValidateTaskParameters (element, elementSyntax, (TaskInfo) elementSymbol); break; case MSBuildSyntaxKind.Property: @@ -424,10 +424,10 @@ void ValidateItemAttributes (MSBuildElementSyntax resolved, XElement element) } } - void ValidateTaskParameters (XElement element, MSBuildElementSyntax resolvedElement, MSBuildTaskElement taskElement) + void ValidateTaskParameters (XElement element, MSBuildElementSyntax resolvedElement, TaskInfo? info) { - var taskName = taskElement.TaskName; - var info = Document.GetSchemas ().GetTask (taskName); + // TODO: the validator should operate on MSBuildTaskElement + var taskName = element.Name.Name!; // taskElement.TaskName if (info is null || info.DeclarationKind == TaskDeclarationKind.Inferred) { Diagnostics.Add (CoreDiagnostics.TaskNotDefined, element.NameSpan, taskName); @@ -446,15 +446,19 @@ void ValidateTaskParameters (XElement element, MSBuildElementSyntax resolvedElem } } - foreach (var att in taskElement.ParameterAttributes) { - if (!info.Parameters.TryGetValue (att.Name, out TaskParameterInfo? pi)) { - Diagnostics.Add (CoreDiagnostics.UnknownTaskParameter, att.XAttribute.NameSpan, taskName, att.Name); + // taskElement.ParameterAttributes + foreach (var att in element.Attributes) { + if (resolvedElement.GetAttribute (att)?.SyntaxKind != MSBuildSyntaxKind.Task_Parameter) { + continue; + } + if (!info.Parameters.TryGetValue (att.Name.Name!, out TaskParameterInfo? pi)) { + Diagnostics.Add (CoreDiagnostics.UnknownTaskParameter, att.NameSpan, taskName, att.Name); continue; } if (pi.IsRequired) { required.Remove (pi.Name); - if (att.Value.IsNullOrEmpty ()) { - Diagnostics.Add (CoreDiagnostics.EmptyRequiredTaskParameter, att.XAttribute.NameSpan, taskName, att.Name); + if (string.IsNullOrEmpty(att.Value)) { + Diagnostics.Add (CoreDiagnostics.EmptyRequiredTaskParameter, att.NameSpan, taskName, att.Name); } } } @@ -463,20 +467,29 @@ void ValidateTaskParameters (XElement element, MSBuildElementSyntax resolvedElem Diagnostics.Add (CoreDiagnostics.MissingRequiredTaskParameter, element.NameSpan, taskName, r); } - foreach (var output in taskElement.OutputElements) { - if (output.TaskParameterAttribute is not { } paramNameAtt || paramNameAtt.Value.IsNullOrEmpty ()) { + // taskElement.OutputElements + foreach (var output in element.Elements) { + if (output.Name.Name is not string outputName || resolvedElement.GetChild (output.Name.Name)?.SyntaxKind != MSBuildSyntaxKind.Output) { + continue; + } + // output.TaskParameterAttribute + if (output.Attributes.Get(AttributeName.TaskParameter, true) is not { } paramNameAtt || string.IsNullOrEmpty(paramNameAtt.Value)) { continue; } + string paramName = paramNameAtt.Value; + // TODO: add this back when we have the expression from the MSBuildAttribute + /* if (paramNameAtt.AsConstString () is not string paramName) { Diagnostics.Add (CoreDiagnostics.UnexpectedExpression, paramNameAtt.GetValueErrorSpan (), "attribute", paramNameAtt.Name); continue; } + */ if (!info.Parameters.TryGetValue (paramName, out TaskParameterInfo? pi)) { - Diagnostics.Add (CoreDiagnostics.UnknownTaskParameter, paramNameAtt.GetValueErrorSpan (), taskName, paramName); + Diagnostics.Add (CoreDiagnostics.UnknownTaskParameter, paramNameAtt.ValueSpan!.Value, taskName, paramName); continue; } if (!pi.IsOutput) { - Diagnostics.Add (CoreDiagnostics.NonOutputTaskParameter, paramNameAtt.GetValueErrorSpan (), taskName, paramName); + Diagnostics.Add (CoreDiagnostics.NonOutputTaskParameter, paramNameAtt.ValueSpan!.Value, taskName, paramName); continue; } } diff --git a/MonoDevelop.MSBuild/Language/MSBuildDocumentVisitor.cs b/MonoDevelop.MSBuild/Language/MSBuildDocumentVisitor.cs index ce458a1f..d22cc678 100644 --- a/MonoDevelop.MSBuild/Language/MSBuildDocumentVisitor.cs +++ b/MonoDevelop.MSBuild/Language/MSBuildDocumentVisitor.cs @@ -128,11 +128,6 @@ void ResolveAttributesAndValue (XElement element, MSBuildElementSyntax elementSy var attributeSymbol = GetSchemas ().GetAttributeInfo (attributeSyntax, elementName, attributeName); - // GetAttributeInfo may have returned a specialized variant of the MSBuildAttributeSyntax, so update it - if (attributeSymbol is MSBuildAttributeSyntax specializedAttributeSyntax) { - attributeSyntax = specializedAttributeSyntax; - } - VisitResolvedAttribute (element, att, elementSyntax, attributeSyntax, elementSymbol, attributeSymbol ?? attributeSyntax); } diff --git a/MonoDevelop.MSBuild/Language/MSBuildInferredSchema.cs b/MonoDevelop.MSBuild/Language/MSBuildInferredSchema.cs index fbefcb05..ff0590a6 100644 --- a/MonoDevelop.MSBuild/Language/MSBuildInferredSchema.cs +++ b/MonoDevelop.MSBuild/Language/MSBuildInferredSchema.cs @@ -285,7 +285,7 @@ void CollectTask (string name) } } - class InferredTaskInfo (string name) : TaskInfo (name, null, TaskDeclarationKind.Inferred, null, null, null, null, 0, null) { } + class InferredTaskInfo (string name) : TaskInfo (name, null, TaskDeclarationKind.Inferred, null, null, null, null, null, null) { } void CollectTaskParameter (string taskName, string parameterName, bool isOutput) { @@ -391,14 +391,14 @@ void CollectTaskDefinition (MSBuildUsingTaskElement element, MSBuildParserContex parseContext.PropertyCollector ); - TaskInfo info = parseContext.TaskBuilder.CreateTaskInfo (fullTaskName, assemblyName, assemblyFile, assemblyFileStr, Filename, element.XElement.Span.Start, evalCtx, parseContext.Logger); + TaskInfo info = parseContext.TaskBuilder.CreateTaskInfo (fullTaskName, assemblyName, assemblyFile, assemblyFileStr, Filename, element.XElement.Span, evalCtx, parseContext.Logger); if (info != null) { Tasks[info.Name] = info; return; } else { // created placeholder task marked as unresolved for analyzers etc - Tasks[taskName] = new TaskInfo (taskName, null, TaskDeclarationKind.AssemblyUnresolved, fullTaskName, assemblyName, assemblyFileStr, Filename, element.XElement.Span.Start, null); + Tasks[taskName] = new TaskInfo (taskName, null, TaskDeclarationKind.AssemblyUnresolved, fullTaskName, assemblyName, assemblyFileStr, Filename, element.XElement.Span, null); return; } } @@ -423,7 +423,7 @@ void CollectTaskDefinition (MSBuildUsingTaskElement element, MSBuildParserContex } } - Tasks[taskName] = new TaskInfo (taskName, null, declarationKind, fullTaskName, null, null, Filename, element.XElement.Span.Start, null, taskParameters); + Tasks[taskName] = new TaskInfo (taskName, null, declarationKind, fullTaskName, null, null, Filename, element.XElement.Span, null, taskParameters); } void ExtractReferences (MSBuildValueKind kind, ExpressionNode expression) diff --git a/MonoDevelop.MSBuild/Language/MSBuildNavigation.cs b/MonoDevelop.MSBuild/Language/MSBuildNavigation.cs index 5950c46a..7bcb58f1 100644 --- a/MonoDevelop.MSBuild/Language/MSBuildNavigation.cs +++ b/MonoDevelop.MSBuild/Language/MSBuildNavigation.cs @@ -61,29 +61,8 @@ public static bool CanNavigate (MSBuildRootDocument doc, int offset, MSBuildReso } var annotations = GetAnnotationsAtOffset (doc, offset); - if (annotations is null) { - return null; - } - - var firstAnnotation = annotations.FirstOrDefault (); - if (firstAnnotation != null) { - var arr = GetAnnotatedPaths ().ToArray (); - if (arr.Length == 0) { - return null; - } - return new MSBuildNavigationResult (arr, firstAnnotation.Span.Start, firstAnnotation.Span.Length); - } - - IEnumerable GetAnnotatedPaths () - { - foreach (var a in annotations) { - if (a.IsSdk) { - yield return Path.Combine (a.Path, "Sdk.props"); - yield return Path.Combine (a.Path, "Sdk.targets"); - } else { - yield return a.Path; - } - } + if (annotations is not null && CreateAnnotationResult (annotations) is { } annotationResult) { + return annotationResult; } if (rr.ReferenceKind == MSBuildReferenceKind.Item) { @@ -109,7 +88,7 @@ IEnumerable GetAnnotatedPaths () if (task?.DeclaredInFile != null) { return new MSBuildNavigationResult ( MSBuildReferenceKind.Task, task.Name, rr.ReferenceOffset, rr.ReferenceLength, - task.DeclaredInFile, task.DeclaredAtOffset + task.DeclaredInFile, task.DeclarationSpan ); } } @@ -132,6 +111,32 @@ IEnumerable GetAnnotatedPaths () .Where (a => !(a is IRegionAnnotation ra) || ra.Span.Contains (offset)); } + static MSBuildNavigationResult? CreateAnnotationResult(IEnumerable annotations) + { + var firstAnnotation = annotations.FirstOrDefault (); + if (firstAnnotation is null) { + return null; + } + + var arr = GetAnnotatedPaths ().ToArray (); + if (arr.Length == 0) { + return null; + } + return new MSBuildNavigationResult (arr, firstAnnotation.Span.Start, firstAnnotation.Span.Length); + + IEnumerable GetAnnotatedPaths () + { + foreach (var a in annotations) { + if (a.IsSdk) { + yield return Path.Combine (a.Path, "Sdk.props"); + yield return Path.Combine (a.Path, "Sdk.targets"); + } else { + yield return a.Path; + } + } + } + } + public static List ResolveAll (MSBuildRootDocument doc, int offset, int length, ILogger logger) { if (doc.XDocument.RootElement is not XElement rootElement) { @@ -150,6 +155,7 @@ public MSBuildNavigationVisitor (MSBuildDocument document, ITextSource textSourc public List Navigations { get; } = new (); + protected override void VisitResolvedAttribute ( XElement element, XAttribute attribute, MSBuildElementSyntax elementSyntax, MSBuildAttributeSyntax attributeSyntax, @@ -158,13 +164,14 @@ protected override void VisitResolvedAttribute ( switch (attributeSyntax.SyntaxKind) { case MSBuildSyntaxKind.Project_Sdk: case MSBuildSyntaxKind.Import_Project: + case MSBuildSyntaxKind.Import_Sdk: + case MSBuildSyntaxKind.Sdk_Name: var annotations = Document.Annotations?.GetMany (attribute); - if (annotations != null) { + if (annotations is not null) { foreach (var group in annotations.GroupBy (a => a.Span.Start)) { - var first = group.First (); - Navigations.Add (new MSBuildNavigationResult ( - group.Select (a => a.Path).ToArray (), first.Span.Start, first.Span.Length - )); + if (CreateAnnotationResult (group) is { } result) { + Navigations.Add (result); + } } } break; @@ -183,19 +190,12 @@ protected override void VisitValue ( switch (valueSymbol.ValueKindWithoutModifiers ()) { case MSBuildValueKind.TargetName: - foreach (var n in node.WithAllDescendants ()) { - if (n is ExpressionText lit && lit.IsPure) { - Navigations.Add (new MSBuildNavigationResult ( - MSBuildReferenceKind.Target, lit.Value, lit.Offset, lit.Length - )); - } - } + CollectList (MSBuildReferenceKind.Target, node); break; case MSBuildValueKind.File: case MSBuildValueKind.FileOrFolder: case MSBuildValueKind.ProjectFile: case MSBuildValueKind.TaskAssemblyFile: - case MSBuildValueKind.Unknown: if (node is ListExpression list) { foreach (var n in list.Nodes) { var p = GetPathFromNode (n, (MSBuildRootDocument)Document, Logger); @@ -211,6 +211,32 @@ protected override void VisitValue ( break; } } + + void Collect (MSBuildReferenceKind kind, string name, int offset, int length) + => Navigations.Add (new MSBuildNavigationResult (kind, name, offset, length)); + void CollectElementName (MSBuildReferenceKind kind, XElement resolvedElement) + => Navigations.Add (new MSBuildNavigationResult (kind, resolvedElement.Name.Name, resolvedElement.NameOffset, resolvedElement.Name.Length)); + + void CollectList (MSBuildReferenceKind kind, ExpressionNode node) + { + if (node is ListExpression list) { + foreach(var child in list.Nodes) { + if (child is ExpressionText text && text.IsPure) { + Collect (kind, text); + } + } + } else if (node is ExpressionText text && text.IsPure) { + Collect (kind, text); + } + } + + void Collect (MSBuildReferenceKind kind, ExpressionText text) + { + string value = text.GetUnescapedValue (true, out int offset, out int length); + if (length > 0) { + Collect (kind, value, offset, length); + } + } } public static MSBuildNavigationResult? GetPathFromNode (ExpressionNode node, MSBuildRootDocument document, ILogger logger) @@ -218,9 +244,7 @@ protected override void VisitValue ( try { var path = MSBuildCompletionExtensions.EvaluateExpressionAsPaths (node, document).FirstOrDefault (); if (path != null && File.Exists (path)) { - return new MSBuildNavigationResult ( - new[] { path }, node.Offset, node.Length - ); + return new MSBuildNavigationResult ([path], node.Offset, node.Length); } } catch (Exception ex) { LogNodePathError (logger, ex); @@ -236,14 +260,14 @@ class MSBuildNavigationResult { public MSBuildNavigationResult ( MSBuildReferenceKind kind, string name, int offset, int length, - string? destFile = null, int destOffset = default) + string? destFile = null, TextSpan? targetSpan = null) { Kind = kind; Name = name; Offset = offset; Length = length; DestFile = destFile; - DestOffset = destOffset; + TargetSpan = targetSpan; } public MSBuildNavigationResult (string[] paths, int offset, int length) @@ -260,6 +284,6 @@ public MSBuildNavigationResult (string[] paths, int offset, int length) public int Length { get; } public string[]? Paths { get; } public string? DestFile { get; } - public int DestOffset { get; } + public TextSpan? TargetSpan { get; } } } diff --git a/MonoDevelop.MSBuild/Language/Typesystem/TaskInfo.cs b/MonoDevelop.MSBuild/Language/Typesystem/TaskInfo.cs index 1685f903..ab1c88d3 100644 --- a/MonoDevelop.MSBuild/Language/Typesystem/TaskInfo.cs +++ b/MonoDevelop.MSBuild/Language/Typesystem/TaskInfo.cs @@ -18,7 +18,7 @@ public class TaskInfo : BaseSymbol, IVersionableSymbol, ITypedSymbol, IHasHelpUr /// Intrinsic task /// internal TaskInfo (string name, DisplayText description, TaskParameterInfo[] parameters, string? helpUrl, string? parametersHelpUrl) - : this (name, description, TaskDeclarationKind.Intrinsic, null, null, null, null, 0, null, helpUrl: helpUrl) + : this (name, description, TaskDeclarationKind.Intrinsic, null, null, null, null, null, null, helpUrl: helpUrl) { this.parameters = new Dictionary (); foreach (var p in parameters) { @@ -30,7 +30,7 @@ internal TaskInfo (string name, DisplayText description, TaskParameterInfo[] par /// /// All other kinds of task /// - public TaskInfo (string name, DisplayText description, TaskDeclarationKind declarationKind, string? typeName, string? assemblyName, string? assemblyFile, string? declaredInFile, int declaredAtOffset, + public TaskInfo (string name, DisplayText description, TaskDeclarationKind declarationKind, string? typeName, string? assemblyName, string? assemblyFile, string? declaredInFile, Xml.Dom.TextSpan? declarationSpan, SymbolVersionInfo? versionInfo, Dictionary? parameters = null, string? helpUrl = null, string? parametersHelpUrl = null ) : base (name, description) @@ -40,7 +40,7 @@ public TaskInfo (string name, DisplayText description, TaskDeclarationKind decla AssemblyName = assemblyName; AssemblyFile = assemblyFile; DeclaredInFile = declaredInFile; - DeclaredAtOffset = declaredAtOffset; + DeclarationSpan = declarationSpan; VersionInfo = versionInfo; this.parameters = parameters ?? new Dictionary (StringComparer.OrdinalIgnoreCase); HelpUrl = helpUrl; @@ -67,7 +67,7 @@ internal void SetParameter (TaskParameterInfo parameterInfo) public string? AssemblyFile { get; } public string? DeclaredInFile { get; } - public int DeclaredAtOffset { get; } + public Xml.Dom.TextSpan? DeclarationSpan { get; } public SymbolVersionInfo? VersionInfo { get; } diff --git a/MonoDevelop.MSBuild/MonoDevelop.MSBuild.csproj b/MonoDevelop.MSBuild/MonoDevelop.MSBuild.csproj index 1e38c7a8..2c474b52 100644 --- a/MonoDevelop.MSBuild/MonoDevelop.MSBuild.csproj +++ b/MonoDevelop.MSBuild/MonoDevelop.MSBuild.csproj @@ -33,6 +33,7 @@ + @@ -42,8 +43,9 @@ --> - - + + + @@ -54,12 +56,12 @@ - + diff --git a/MonoDevelop.MSBuild/NoopTaskMetadataBuilder.cs b/MonoDevelop.MSBuild/NoopTaskMetadataBuilder.cs index a44fd669..113c6216 100644 --- a/MonoDevelop.MSBuild/NoopTaskMetadataBuilder.cs +++ b/MonoDevelop.MSBuild/NoopTaskMetadataBuilder.cs @@ -13,7 +13,7 @@ class NoopTaskMetadataBuilder : ITaskMetadataBuilder { public TaskInfo CreateTaskInfo ( string typeName, string assemblyName, ExpressionNode assemblyFile, string assemblyFileStr, - string declaredInFile, int declaredAtOffset, IMSBuildEvaluationContext evaluationContext, ILogger logger) + string declaredInFile, Xml.Dom.TextSpan? declarationSpan, IMSBuildEvaluationContext evaluationContext, ILogger logger) { return null; } diff --git a/MonoDevelop.MSBuild/PackageSearch/.editorconfig b/MonoDevelop.MSBuild/PackageSearch/.editorconfig new file mode 100644 index 00000000..1fe8c7a3 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: http://EditorConfig.org + +[*.cs] + +# revert settings to match roslyn style better +indent_style = space +trim_trailing_whitespace = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_keywords_in_control_flow_statements = false + +# Newline settings +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false + +# VS threading analyzer triggers on imported roslyn code +dotnet_diagnostic.VSTHRD002.severity = none +dotnet_diagnostic.VSTHRD003.severity = none +dotnet_diagnostic.VSTHRD103.severity = none +dotnet_diagnostic.VSTHRD110.severity = none \ No newline at end of file diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IDependencyManager.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IDependencyManager.cs new file mode 100644 index 00000000..de7862ee --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IDependencyManager.cs @@ -0,0 +1,13 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IDependencyManager +{ + T GetComponent(); + + IReadOnlyList GetComponents(); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeed.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeed.cs new file mode 100644 index 00000000..f9940eac --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeed.cs @@ -0,0 +1,18 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeed +{ + string DisplayName { get; } + + Task GetPackageNamesAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken); + + Task GetPackageVersionsAsync(string id, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken); + + Task GetPackageInfoAsync(string id, string? version, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactory.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactory.cs new file mode 100644 index 00000000..84a9e462 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactory.cs @@ -0,0 +1,9 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeedFactory +{ + bool TryHandle(string feed, out IPackageFeed instance); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactorySelector.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactorySelector.cs new file mode 100644 index 00000000..43e265ab --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedFactorySelector.cs @@ -0,0 +1,13 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeedFactorySelector +{ + IEnumerable FeedFactories { get; } + + IPackageFeed GetFeed(string source); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedRegistryProvider.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedRegistryProvider.cs new file mode 100644 index 00000000..5b1c41cf --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedRegistryProvider.cs @@ -0,0 +1,11 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeedRegistryProvider +{ + IReadOnlyList ConfiguredFeeds { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearchJob.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearchJob.cs new file mode 100644 index 00000000..765b17b5 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearchJob.cs @@ -0,0 +1,22 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeedSearchJob +{ + IReadOnlyList RemainingFeeds { get; } + + IReadOnlyList SearchingIn { get; } + + IReadOnlyList Results { get; } + + bool IsCancelled { get; } + + event EventHandler Updated; + + void Cancel(); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearcher.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearcher.cs new file mode 100644 index 00000000..b6ea9ab7 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageFeedSearcher.cs @@ -0,0 +1,13 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageFeedSearcher +{ + Task SearchPackagesAsync(string prefix, params string[] feeds); + + Task SearchVersionsAsync(string prefix, params string[] feeds); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageInfo.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageInfo.cs new file mode 100644 index 00000000..c8d32494 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageInfo.cs @@ -0,0 +1,34 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageInfo +{ + string Id { get; } + + string Version { get; } + + string Title { get; } + + string Authors { get; } + + string Summary { get; } + + string Description { get; } + + string LicenseUrl { get; } + + string ProjectUrl { get; } + + string IconUrl { get; } + + string Tags { get; } + + IReadOnlyList PackageTypes { get; } + + FeedKind SourceKind { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageNameSearchResult.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageNameSearchResult.cs new file mode 100644 index 00000000..743bbc51 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageNameSearchResult.cs @@ -0,0 +1,16 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageNameSearchResult +{ + bool Success { get; } + + FeedKind SourceKind { get; } + + IReadOnlyList Names { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageQueryConfiguration.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageQueryConfiguration.cs new file mode 100644 index 00000000..0e760b98 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageQueryConfiguration.cs @@ -0,0 +1,17 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageQueryConfiguration +{ + string? CompatibilityTarget { get; } + + bool IncludePreRelease { get; } + + int MaxResults { get; } + + PackageType? PackageType { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageSearchManager.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageSearchManager.cs new file mode 100644 index 00000000..a0d49989 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageSearchManager.cs @@ -0,0 +1,18 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageSearchManager +{ + IPackageFeedSearchJob> SearchPackageNames(string prefix, string? tfm, string? packageType = null); + + IPackageFeedSearchJob> SearchPackageVersions(string packageName, string? tfm, string? packageType = null); + + IPackageFeedSearchJob SearchPackageInfo(string packageId, string? version, string? tfm); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageVersionSearchResult.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageVersionSearchResult.cs new file mode 100644 index 00000000..d83497a9 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/IPackageVersionSearchResult.cs @@ -0,0 +1,16 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public interface IPackageVersionSearchResult +{ + bool Success { get; } + + IReadOnlyList Versions { get; } + + FeedKind SourceKind { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Contracts/PackageType.cs b/MonoDevelop.MSBuild/PackageSearch/Contracts/PackageType.cs new file mode 100644 index 00000000..e6b4db7b --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Contracts/PackageType.cs @@ -0,0 +1,50 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Generic; + +namespace ProjectFileTools.NuGetSearch.Contracts; + +public class PackageType : IEquatable +{ + public static IReadOnlyList DefaultList { get; } = new[] { KnownPackageType.Dependency }; + + public PackageType(string id, string? version = null) + { + Name = id ?? throw new ArgumentNullException(nameof(id)); + Version = version; + } + public string Name { get; } + public string? Version { get; } + + + public override bool Equals(object? obj) => Equals(obj as PackageType); + + public bool Equals(PackageType? other) => other is PackageType + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && string.Equals(Version, other.Version, StringComparison.Ordinal); + + public override int GetHashCode() + { + int hashCode = -612338121; + hashCode = hashCode * -1521134295 + StringComparer.Ordinal.GetHashCode(Name); + if (Version is not null) { + hashCode = hashCode * -1521134295 + StringComparer.Ordinal.GetHashCode(Version); + } + return hashCode; + } +} + +public class KnownPackageType +{ + public static PackageType Legacy { get; } = new PackageType("Legacy"); + public static PackageType DotnetCliTool { get; } = new PackageType("DotnetCliTool"); + public static PackageType Dependency { get; } = new PackageType("Dependency"); + public static PackageType DotnetTool { get; } = new PackageType("DotnetTool"); + public static PackageType SymbolsPackage { get; } = new PackageType("SymbolsPackage"); + public static PackageType DotnetPlatform { get; } = new PackageType("DotnetPlatform"); + public static PackageType MSBuildSdk { get; } = new PackageType("MSBuildSdk"); +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetDiskFeedFactory.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetDiskFeedFactory.cs new file mode 100644 index 00000000..88804127 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetDiskFeedFactory.cs @@ -0,0 +1,92 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.NuGetSearch.Feeds.Disk; + +public class NuGetDiskFeedFactory : IPackageFeedFactory +{ + private static readonly ConcurrentDictionary Instances = new ConcurrentDictionary(); + private readonly IFileSystem _fileSystem; + + public NuGetDiskFeedFactory(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool TryHandle(string feed, out IPackageFeed instance) + { + if (!_fileSystem.DirectoryExists(feed)) + { + instance = null; + return false; + } + + instance = Instances.GetOrAdd(feed, x => CreateInstance(x)); + return instance != null; + } + + private IPackageFeed CreateInstance(string feed) + { + if (IsNuGetV3Feed(feed)) + { + return Instances.GetOrAdd(feed, x => new NuGetV3DiskFeed(x, _fileSystem)); + } + + if (IsNuGetV2Feed(feed)) + { + return Instances.GetOrAdd(feed, x => new NuGetV2DiskFeed(x, _fileSystem)); + } + + return null; + } + + private bool IsNuGetV2Feed(string feed) + { + foreach (string dir in _fileSystem.EnumerateDirectories(feed, "*", SearchOption.TopDirectoryOnly)) + { + if (_fileSystem.EnumerateFiles(dir, "*.nupkg", SearchOption.TopDirectoryOnly).Any()) + { + return true; + } + } + + return false; + } + + private bool IsNuGetV3Feed(string feed) + { + foreach (string dir in _fileSystem.EnumerateDirectories(feed, "*", SearchOption.TopDirectoryOnly)) + { + if (_fileSystem.EnumerateFiles(dir, "*.nuspec", SearchOption.TopDirectoryOnly).Any()) + { + return false; + } + + if(_fileSystem.GetDirectoryName(dir).IndexOf('.') == 0) + { + continue; + } + + foreach (string sub in _fileSystem.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly)) + { + if (SemanticVersion.Parse(_fileSystem.GetDirectoryName(sub)) == null) + { + return false; + } + + if (!_fileSystem.EnumerateFiles(sub, "*.nuspec", SearchOption.TopDirectoryOnly).Any()) + { + return false; + } + } + } + + return true; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetPackageMatcher.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetPackageMatcher.cs new file mode 100644 index 00000000..9b8ba1b2 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetPackageMatcher.cs @@ -0,0 +1,36 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; + +namespace ProjectFileTools.NuGetSearch.Feeds.Disk; + +internal static class NuGetPackageMatcher +{ + public static bool IsMatch(string dir, IPackageInfo info, IPackageQueryConfiguration queryConfiguration, IFileSystem fileSystem) + { + if (!queryConfiguration.IncludePreRelease) + { + SemanticVersion ver = SemanticVersion.Parse(info.Version); + + if(!string.IsNullOrEmpty(ver?.PrereleaseVersion)) + { + return false; + } + } + + if (queryConfiguration.PackageType != null) + { + //NOTE: can't find any info on how the version is supposed to be handled (or what it's even for), so use an exact match + if (!info.PackageTypes.Any(p => queryConfiguration.PackageType.Equals(p))) + { + return false; + } + } + + return true; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV2DiskFeed.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV2DiskFeed.cs new file mode 100644 index 00000000..d4e68e2c --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV2DiskFeed.cs @@ -0,0 +1,165 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; +using ProjectFileTools.NuGetSearch.Search; + +namespace ProjectFileTools.NuGetSearch.Feeds.Disk; + +internal class NuGetV2DiskFeed : IPackageFeed +{ + private readonly string _feed; + private readonly bool _isRemote; + private readonly IFileSystem _fileSystem; + + public NuGetV2DiskFeed(string feed, IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _feed = feed; + _isRemote = Uri.TryCreate(feed, UriKind.Absolute, out Uri uri) && uri.IsUnc; + } + + public string DisplayName + { + get + { + if (_isRemote) + { + return $"NuGet v2 (Remote: {_feed})"; + } + + return $"NuGet v2 (Local: {_feed})"; + } + } + + public Task GetPackageInfoAsync(string id, string version, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + if (version != null) + { + string nuspec = Path.Combine(_feed, $"{id}.{version}", $"{id}.nuspec"); + + if (_fileSystem.FileExists(nuspec)) + { + return Task.FromResult(NuSpecReader.Read(nuspec, FeedKind.Local)); + } + else + { + return Task.FromResult(null); + } + } + else + { + string dir = _fileSystem.EnumerateDirectories(_feed).OrderByDescending(x => SemanticVersion.Parse(_fileSystem.GetDirectoryNameOnly(x).Substring(id.Length + 1))).FirstOrDefault(); + + if (dir == null) + { + return Task.FromResult(null); + } + + string nuspec = Path.Combine(dir, $"{id}.nuspec"); + + if (_fileSystem.FileExists(nuspec)) + { + return Task.FromResult(NuSpecReader.Read(nuspec, FeedKind.Local)); + } + else + { + return Task.FromResult(null); + } + } + }, cancellationToken); + } + + public Task GetPackageNamesAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + try + { + List infos = new List(); + foreach (string path in _fileSystem.EnumerateDirectories(_feed).Where(x => x.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) > -1)) + { + if (cancellationToken.IsCancellationRequested) + { + return PackageNameSearchResult.Cancelled; + } + + string nuspec = _fileSystem.EnumerateFiles(path, "*.nuspec", SearchOption.TopDirectoryOnly).FirstOrDefault(); + + if (nuspec != null) + { + IPackageInfo info = NuSpecReader.Read(nuspec, FeedKind.Local); + + if (info != null && NuGetPackageMatcher.IsMatch(path, info, queryConfiguration, _fileSystem)) + { + infos.Add(info); + + if (infos.Count >= queryConfiguration.MaxResults) + { + break; + } + } + } + } + + return new PackageNameSearchResult(infos.Select(x => x.Id).ToList(), FeedKind.Local); + } + catch + { + return PackageNameSearchResult.Failure; + } + }, cancellationToken); + } + + public Task GetPackageVersionsAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + try + { + List versions = new List(); + bool anyFound = false; + foreach (string path in _fileSystem.EnumerateDirectories(_feed).Where(x => x.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) > -1)) + { + if (cancellationToken.IsCancellationRequested) + { + return PackageVersionSearchResult.Cancelled; + } + + string nuspec = _fileSystem.EnumerateFiles(path, "*.nuspec", SearchOption.TopDirectoryOnly).FirstOrDefault(); + + if (nuspec != null) + { + anyFound = true; + IPackageInfo info = NuSpecReader.Read(nuspec, FeedKind.Local); + + if (info != null && string.Equals(info.Id, prefix, StringComparison.OrdinalIgnoreCase)) + { + versions.Add(info.Version); + } + } + } + + if (anyFound) + { + return new PackageVersionSearchResult(versions, FeedKind.Local); + } + + return PackageVersionSearchResult.Failure; + } + catch + { + return PackageVersionSearchResult.Failure; + } + }, cancellationToken); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV3DiskFeed.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV3DiskFeed.cs new file mode 100644 index 00000000..a143ddc5 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Disk/NuGetV3DiskFeed.cs @@ -0,0 +1,173 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; +using ProjectFileTools.NuGetSearch.Search; + +namespace ProjectFileTools.NuGetSearch.Feeds.Disk; + +internal class NuGetV3DiskFeed : IPackageFeed +{ + private readonly string _feed; + private readonly IFileSystem _fileSystem; + private readonly bool _isRemote; + + public NuGetV3DiskFeed(string feed, IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _feed = feed; + _isRemote = Uri.TryCreate(feed, UriKind.Absolute, out Uri uri) && uri.IsUnc; + } + + public string DisplayName + { + get + { + if (_isRemote) + { + return $"NuGet v3 (Remote: {_feed})"; + } + + return $"NuGet v3 (Local: {_feed})"; + } + } + + public Task GetPackageInfoAsync(string id, string version, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + if (version != null) + { + string nuspec = Path.Combine(_feed, id, version, $"{id}.nuspec"); + + if (_fileSystem.FileExists(nuspec)) + { + return Task.FromResult(NuSpecReader.Read(nuspec, FeedKind.Local)); + } + else + { + return Task.FromResult(null); + } + } + else + { + string package = Path.Combine(_feed, id); + string dir = _fileSystem.EnumerateDirectories(package).OrderByDescending(x => SemanticVersion.Parse(_fileSystem.GetDirectoryName(x))).FirstOrDefault(); + + if (dir == null) + { + return Task.FromResult(null); + } + + string nuspec = Path.Combine(dir, $"{id}.nuspec"); + + if (_fileSystem.FileExists(nuspec)) + { + return Task.FromResult(NuSpecReader.Read(nuspec, FeedKind.Local)); + } + else + { + return Task.FromResult(null); + } + } + }, cancellationToken); + } + + public Task GetPackageNamesAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + try + { + List infos = new List(); + foreach (string path in _fileSystem.EnumerateDirectories(_feed, $"*{prefix}*", SearchOption.TopDirectoryOnly).Where(x => x.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) > -1)) + { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (infos.Count >= queryConfiguration.MaxResults) + { + break; + } + + foreach (string verDir in _fileSystem.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)) + { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (verDir == null || SemanticVersion.Parse(verDir) == null) + { + continue; + } + + string nuspec = _fileSystem.EnumerateFiles(verDir, "*.nuspec", SearchOption.TopDirectoryOnly).FirstOrDefault(); + + if (nuspec != null) + { + IPackageInfo info = NuSpecReader.Read(nuspec, FeedKind.Local); + if (info != null && NuGetPackageMatcher.IsMatch(verDir, info, queryConfiguration, _fileSystem)) + { + infos.Add(info); + + if (infos.Count >= queryConfiguration.MaxResults) + { + break; + } + } + } + } + } + + return new PackageNameSearchResult(infos.Select(x => x.Id).ToList(), FeedKind.Local); + } + catch + { + return PackageNameSearchResult.Failure; + } + }, cancellationToken); + } + + public Task GetPackageVersionsAsync(string id, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + return Task.Run(() => + { + string packagePath = Path.Combine(_feed, id.ToLowerInvariant()); + if (!_fileSystem.DirectoryExists(packagePath)) + { + return PackageVersionSearchResult.Failure; + } + + try + { + List versions = new List(); + + foreach (string directory in _fileSystem.EnumerateDirectories(packagePath, "*", SearchOption.TopDirectoryOnly)) + { + string version = SemanticVersion.Parse(_fileSystem.GetDirectoryNameOnly(directory))?.ToString(); + + if (version != null) + { + versions.Add(version); + } + } + + return new PackageVersionSearchResult(versions, FeedKind.Local); + } + catch + { + return PackageVersionSearchResult.Failure; + } + }, cancellationToken); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/FeedKind.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/FeedKind.cs new file mode 100644 index 00000000..2d2f0a4b --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/FeedKind.cs @@ -0,0 +1,11 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ProjectFileTools.NuGetSearch.Feeds; + +public enum FeedKind +{ + NuGet, + MyGet, + Local +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/NuSpecReader.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/NuSpecReader.cs new file mode 100644 index 00000000..001a4254 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/NuSpecReader.cs @@ -0,0 +1,53 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Xml.Linq; +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Feeds; + +internal class NuSpecReader +{ + internal static IPackageInfo Read(string nuspec, FeedKind kind) + { + XDocument doc = XDocument.Load(nuspec); + XNamespace ns = doc.Root.GetDefaultNamespace(); + XElement package = doc.Root; + XElement metadata = package?.Element(XName.Get("metadata", ns.NamespaceName)); + XElement id = metadata?.Element(XName.Get("id", ns.NamespaceName)); + XElement version = metadata?.Element(XName.Get("version", ns.NamespaceName)); + XElement title = metadata?.Element(XName.Get("title", ns.NamespaceName)); + XElement authors = metadata?.Element(XName.Get("authors", ns.NamespaceName)); + XElement summary = metadata?.Element (XName.Get ("summary", ns.NamespaceName)); + XElement description = metadata?.Element(XName.Get("description", ns.NamespaceName)); + XElement licenseUrl = metadata?.Element(XName.Get("licenseUrl", ns.NamespaceName)); + XElement projectUrl = metadata?.Element (XName.Get ("projectUrl", ns.NamespaceName)); + XElement iconUrl = metadata?.Element (XName.Get ("iconUrl", ns.NamespaceName)); + XElement tags = metadata?.Element(XName.Get("tags", ns.NamespaceName)); + + List packageTypes = null; + XElement packageTypesEl = metadata?.Element(XName.Get("packageTypes", ns.NamespaceName)); + if (packageTypesEl != null) + { + var nameName = XName.Get("name"); + var versionName = XName.Get("version"); + packageTypes = new List(); + foreach (var packageType in packageTypesEl.Elements(XName.Get("packageType", ns.NamespaceName))) + { + var typeName = packageType.Attribute(nameName).Value; + var typeVersion = packageType.Attribute(versionName)?.Value; + packageTypes.Add (new PackageType(typeName, typeVersion)); + } + } + + if (id != null) + { + return new PackageInfo( + id.Value, version?.Value, title?.Value, authors?.Value, summary?.Value, description?.Value, + licenseUrl?.Value, projectUrl?.Value, iconUrl?.Value, tags?.Value, kind, packageTypes); + } + + return null; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactoryBase.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactoryBase.cs new file mode 100644 index 00000000..116873f1 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactoryBase.cs @@ -0,0 +1,28 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Feeds; + +internal abstract class PackageFeedFactoryBase : IPackageFeedFactory +{ + private static readonly ConcurrentDictionary Instances = new ConcurrentDictionary(); + + protected abstract bool CanHandle(string feed); + + protected abstract IPackageFeed Create(string feed); + + public virtual bool TryHandle(string feed, out IPackageFeed instance) + { + if (!CanHandle(feed)) + { + instance = null; + return false; + } + + instance = Instances.GetOrAdd(feed, Create); + return true; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactorySelector.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactorySelector.cs new file mode 100644 index 00000000..322c9f58 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageFeedFactorySelector.cs @@ -0,0 +1,41 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Feeds; + +public class PackageFeedFactorySelector : IPackageFeedFactorySelector +{ + private readonly ConcurrentDictionary _feedCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + public PackageFeedFactorySelector(IEnumerable feedFactories) + { + FeedFactories = feedFactories; + } + + public IEnumerable FeedFactories { get; } + + public IPackageFeed GetFeed(string source) + { + if (_feedCache.TryGetValue(source, out IPackageFeed match)) + { + return match; + } + + foreach(IPackageFeedFactory feed in FeedFactories) + { + if (feed.TryHandle(source, out IPackageFeed instance)) + { + _feedCache[source] = instance; + return instance; + } + } + + _feedCache[source] = null; + return null; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageInfo.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageInfo.cs new file mode 100644 index 00000000..f647c583 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageInfo.cs @@ -0,0 +1,50 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Feeds; + +public class PackageInfo : IPackageInfo +{ + public PackageInfo(string id, string version, string title, string authors, string summary, string description, string licenseUrl, string projectUrl, string iconUrl, string tags, FeedKind sourceKind, IReadOnlyList packageTypes) + { + Id = id; + Version = version; + Title = title; + Authors = authors; + Description = description; + LicenseUrl = licenseUrl; + ProjectUrl = projectUrl; + SourceKind = sourceKind; + IconUrl = iconUrl; + Tags = tags; + PackageTypes = packageTypes ?? PackageType.DefaultList; + } + + public string Id { get; } + + public string Version { get; } + + public string Title { get; } + + public string Authors { get; } + + public string Summary { get; } + + public string Description { get; } + + public string LicenseUrl { get; } + + public string ProjectUrl { get; } + + public string IconUrl { get; } + + public string Tags { get; } + + public FeedKind SourceKind { get; } + + public IReadOnlyList PackageTypes { get; } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageQueryConfiguration.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageQueryConfiguration.cs new file mode 100644 index 00000000..6d5c4d49 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/PackageQueryConfiguration.cs @@ -0,0 +1,41 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Feeds; + +public class PackageQueryConfiguration : IPackageQueryConfiguration +{ + public PackageQueryConfiguration(string? targetFrameworkMoniker, bool includePreRelease = true, int maxResults = 100, PackageType? packageType = null) + { + CompatibilityTarget = targetFrameworkMoniker; + IncludePreRelease = includePreRelease; + MaxResults = maxResults; + PackageType = packageType; + } + + public string? CompatibilityTarget { get; } + + public bool IncludePreRelease { get; } + + public int MaxResults { get; } + + public PackageType? PackageType { get; } + + public override int GetHashCode() + { + return (CompatibilityTarget?.GetHashCode() ?? 0) ^ IncludePreRelease.GetHashCode() ^ MaxResults.GetHashCode() ^ (PackageType?.GetHashCode() ?? 0); + } + + public override bool Equals(object? obj) + { + return obj is PackageQueryConfiguration cfg + && string.Equals(CompatibilityTarget, cfg.CompatibilityTarget, System.StringComparison.Ordinal) + && IncludePreRelease == cfg.IncludePreRelease + && MaxResults == cfg.MaxResults + && (PackageType?.Equals(cfg.PackageType) ?? false); + } +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV2ServiceFeed.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV2ServiceFeed.cs new file mode 100644 index 00000000..87f77f36 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV2ServiceFeed.cs @@ -0,0 +1,203 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; +using ProjectFileTools.NuGetSearch.Search; + +namespace ProjectFileTools.NuGetSearch.Feeds.Web; + +public class NuGetV2ServiceFeedFactory : IPackageFeedFactory +{ + private readonly IWebRequestFactory _webRequestFactory; + + public NuGetV2ServiceFeedFactory(IWebRequestFactory webRequestFactory) + { + _webRequestFactory = webRequestFactory; + } + + public bool TryHandle(string feed, out IPackageFeed instance) + { + if (Uri.TryCreate(feed, UriKind.Absolute, out Uri location) && feed.EndsWith("nuget", StringComparison.OrdinalIgnoreCase)) + { + instance = new NuGetV2ServiceFeed(feed, _webRequestFactory); + return true; + } + + instance = null; + return false; + } +} + +public class NuGetV2ServiceFeed : IPackageFeed +{ + private readonly string _feed; + private readonly FeedKind _kind; + private readonly IWebRequestFactory _webRequestFactory; + + public NuGetV2ServiceFeed(string feed, IWebRequestFactory webRequestFactory) + { + _webRequestFactory = webRequestFactory; + _feed = feed; + _kind = feed.IndexOf("nuget.org", StringComparison.OrdinalIgnoreCase) > -1 ? FeedKind.NuGet : FeedKind.MyGet; + } + + public string DisplayName => $"{_feed} (NuGet v2)"; + + + public async Task GetPackageNamesAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return PackageNameSearchResult.Failure; + } + + IReadOnlyList results = new List(); + string frameworkQuery = !string.IsNullOrEmpty(queryConfiguration.CompatibilityTarget) ? $"&targetFramework={queryConfiguration.CompatibilityTarget}" : ""; + var serviceEndpoint = $"{_feed}/Search()"; + Func queryFunc = x => $"{x}?searchTerm='{prefix}'{frameworkQuery}&includePrerelease={queryConfiguration.IncludePreRelease}&semVerLevel=2.0.0"; + XDocument document = await ExecuteAutocompleteServiceQueryAsync(serviceEndpoint, queryFunc, cancellationToken).ConfigureAwait(false); + + if (document != null) + { + try + { + results = GetPackageNamesFromNuGetV2CompatibleQueryResult(document); + return new PackageNameSearchResult(results, _kind); + } + catch + { + } + } + + return PackageNameSearchResult.Failure; + } + + public async Task GetPackageInfoAsync(string packageId, string version, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + if (packageId == null) + { + return null; + } + + var serviceEndpoint = $"{_feed}/Packages(Id='{packageId}',Version='{version}')"; + Func queryFunc = x => $"{x}"; + XDocument document = await ExecuteAutocompleteServiceQueryAsync(serviceEndpoint, queryFunc, cancellationToken).ConfigureAwait(false); + + if (document != null) + { + var el = document.Root; + + var id = GetPropertyValue(document, el, "Id"); + var title = GetPropertyValue(document, el, "Title"); + var authors = GetPropertyValue(document, el, "Authors"); + var summary = GetPropertyValue(document, el, "Summary"); + var description = GetPropertyValue(document, el, "Description"); + var projectUrl = GetPropertyValue(document, el, "ProjectUrl"); + var licenseUrl = GetPropertyValue(document, el, "LicenseUrl"); + var iconUrl = GetPropertyValue(document, el, "IconUrl"); + var tags = GetPropertyValue(document, el, "Tags"); + var packageInfo = new PackageInfo(id, version, title, authors, summary, description, licenseUrl, projectUrl, iconUrl, tags, _kind, null); + return packageInfo; + } + + + return null; + } + + public async Task GetPackageVersionsAsync(string id, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + IReadOnlyList results = new List(); + var serviceEndpoint = $"{_feed}/FindPackagesById()"; + Func queryFunc = x => $"{x}?id='{id}'"; + XDocument document = await ExecuteAutocompleteServiceQueryAsync(serviceEndpoint, queryFunc, cancellationToken).ConfigureAwait(false); + + try + { + results = GetPackageVersionsFromNuGetV2CompatibleQueryResult(id, document); + } + catch + { + return PackageVersionSearchResult.Failure; + } + + return new PackageVersionSearchResult(results, _kind); + } + + private async Task ExecuteAutocompleteServiceQueryAsync(string endpoint, Func query, CancellationToken cancellationToken) + { + if (endpoint == null) + { + return null; + } + + try + { + string location = query(endpoint); + var xml = await _webRequestFactory.GetStringAsync(location, cancellationToken).ConfigureAwait(false); + return XDocument.Parse(xml); + } + catch (Exception) + { + } + + return null; + } + + + private static string GetPropertyValue(XDocument document, XElement el, string propertyKey) + { + return el + .Element(document.Root.GetNamespaceOfPrefix("m") + "properties") + .Element(document.Root.GetNamespaceOfPrefix("d") + propertyKey) + ?.Value; + } + + + + private static IReadOnlyList GetPackageVersionsFromNuGetV2CompatibleQueryResult(string id, XDocument document) + { + List results = new List(); + + if (document != null) + { + foreach (var el in document.Root.Elements(document.Root.GetDefaultNamespace() + "entry")) + { + var pkgId = GetPropertyValue(document, el, "Id"); + + var pkgVersion = GetPropertyValue(document, el, "Version"); + + if (pkgId.Equals(pkgId, StringComparison.OrdinalIgnoreCase)) + { + results.Add(pkgVersion); + } + } + } + + return results; + } + + private static IReadOnlyList GetPackageNamesFromNuGetV2CompatibleQueryResult(XDocument document) + { + List results = new List(); + + if (document != null) + { + foreach (var el in document.Root.Elements(document.Root.GetDefaultNamespace() + "entry")) + { + var id = GetPropertyValue(document, el, "Id"); + + results.Add(id); + } + } + + return results.Distinct().ToList(); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV3ServiceFeed.cs b/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV3ServiceFeed.cs new file mode 100644 index 00000000..550b406c --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Feeds/Web/NuGetV3ServiceFeed.cs @@ -0,0 +1,374 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.IO; +using ProjectFileTools.NuGetSearch.Search; + +namespace ProjectFileTools.NuGetSearch.Feeds.Web; + +public class NuGetV3ServiceFeedFactory : IPackageFeedFactory +{ + private readonly IWebRequestFactory _webRequestFactory; + + public NuGetV3ServiceFeedFactory(IWebRequestFactory webRequestFactory) + { + _webRequestFactory = webRequestFactory; + } + + public bool TryHandle(string feed, out IPackageFeed instance) + { + if (Uri.TryCreate(feed, UriKind.Absolute, out Uri location) && feed.EndsWith("v3/index.json", StringComparison.OrdinalIgnoreCase)) + { + instance = new NuGetV3ServiceFeed(feed, _webRequestFactory); + return true; + } + + instance = null; + return false; + } +} + +public class NuGetV3ServiceFeed : IPackageFeed +{ + private readonly string _feed; + private readonly FeedKind _kind; + private readonly IWebRequestFactory _webRequestFactory; + + public NuGetV3ServiceFeed(string feed, IWebRequestFactory webRequestFactory) + { + _webRequestFactory = webRequestFactory; + _feed = feed; + _kind = feed.IndexOf("nuget.org", StringComparison.OrdinalIgnoreCase) > -1 ? FeedKind.NuGet : FeedKind.MyGet; + } + + public string DisplayName => $"{_feed} (NuGet v3)"; + + private async Task ExecuteAutocompleteServiceQueryAsync(List endpoints, Func query, CancellationToken cancellationToken) + { + if (endpoints == null) + { + return null; + } + + for (int i = 0; i < endpoints.Count; ++i) + { + string endpoint = endpoints[0]; + + try + { + string location = query(endpoint); + return await _webRequestFactory.GetJsonAsync(location, cancellationToken).ConfigureAwait(false) as JObject; + } + catch (Exception) + { + if (endpoints.Count > 1) + { + endpoints.RemoveAt(0); + endpoints.Add(endpoint); + } + + //TODO: Possibly log the failure to get the document + } + } + + return null; + } + + private async Task> DiscoverEndpointsAsync(string packageSource, string autoCompleteServiceTypeIdentifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(packageSource)) + { + return null; + } + + string requestUrl = packageSource.TrimEnd('/'); + + //If we're already requesting /vN/index.json, use it. Otherwise, try to fix up the url to discover an + // index.json file to get endpoints from + if (!requestUrl.EndsWith("/index.json", StringComparison.OrdinalIgnoreCase)) + { + string baseUrl = packageSource; + if (packageSource[packageSource.Length - 1] != '/') + { + baseUrl += "/"; + } + + if (baseUrl.Length < 2) + { + return null; + } + + int lastSlashIndex = baseUrl.LastIndexOf('/', baseUrl.Length - 2); + + //If the only slash in the value we're processing is the one we just added, + // there's something wrong, quit + if (lastSlashIndex < 0) + { + return null; + } + + string lastSegment = baseUrl.Substring(lastSlashIndex + 1); + + //If the base url ended in "v2", move to "v3" + if (string.Equals(lastSegment, "v2/", StringComparison.OrdinalIgnoreCase)) + { + baseUrl = baseUrl.Substring(0, lastSlashIndex + 1) + "v3/"; + } + //If the base url didn't include an understood version identifier, add v3 + else if (!string.Equals(lastSegment, "v3/", StringComparison.OrdinalIgnoreCase)) + { + baseUrl += "v3/"; + } + + requestUrl = baseUrl + "index.json"; + } + + try + { + JObject responseJson = await _webRequestFactory.GetJsonAsync(requestUrl, cancellationToken).ConfigureAwait(false) as JObject; + return FindEndpointsInIndexJSON(responseJson, autoCompleteServiceTypeIdentifier); + } + catch + { + return null; + } + } + + private static List FindEndpointsInIndexJSON(JObject document, string serviceTypeIdentifier) + { + List endpoints = new List(); + + JArray resourcesArray = document?["resources"] as JArray; + + if (resourcesArray == null) + { + return endpoints; + } + + foreach (JToken curResource in resourcesArray) + { + JObject curObject = curResource as JObject; + JArray typeArray = curObject?["@type"] as JArray; + bool foundMatchingService = false; + + if (typeArray != null) + { + foreach (JToken curToken in typeArray) + { + if (string.Equals(curToken.ToString(), serviceTypeIdentifier, StringComparison.Ordinal)) + { + foundMatchingService = true; + break; + } + } + } + else if (curObject != null) + { + // NuGet team indicated a desired to handle fallback scenario where this is a simple string also + if (string.Equals(curObject["@type"].ToString(), serviceTypeIdentifier, StringComparison.Ordinal)) + { + foundMatchingService = true; + } + } + + if (curObject != null && foundMatchingService) + { + endpoints.Add(curObject["@id"].ToString()); + } + } + + return endpoints; + } + + public async Task GetPackageNamesAsync(string prefix, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return PackageNameSearchResult.Failure; + } + + IReadOnlyList results = new List(); + string frameworkQuery = !string.IsNullOrEmpty(queryConfiguration.CompatibilityTarget) ? $"&supportedFramework={queryConfiguration.CompatibilityTarget}" : ""; + string packageTypeQuery = !string.IsNullOrEmpty(queryConfiguration.PackageType?.Name) ? $"&packageType={queryConfiguration.PackageType.Name}" : ""; + const string autoCompleteServiceTypeIdentifier = "SearchAutocompleteService/3.5.0"; + List serviceEndpoints = await DiscoverEndpointsAsync(_feed, autoCompleteServiceTypeIdentifier, cancellationToken).ConfigureAwait(false); + Func queryFunc = x => $"{x}?q={prefix}&semVerLevel=2.0.0{frameworkQuery}{packageTypeQuery}&take={queryConfiguration.MaxResults}&prerelease={queryConfiguration.IncludePreRelease}"; + JObject document = await ExecuteAutocompleteServiceQueryAsync(serviceEndpoints, queryFunc, cancellationToken).ConfigureAwait(false); + + if (document != null) + { + try + { + results = GetDataFromNuGetV3CompatibleQueryResult(document); + return new PackageNameSearchResult(results, _kind); + } + catch + { + } + } + + return PackageNameSearchResult.Failure; + } + + public async Task GetPackageInfoAsync(string packageId, string version, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + if (packageId == null) + { + return null; + } + + string packageDisplayMetadataUriTemplateIdentifier = "PackageDisplayMetadataUriTemplate/3.0.0-rc"; + List packageQuickInfoAddresses = await DiscoverEndpointsAsync(_feed, packageDisplayMetadataUriTemplateIdentifier, cancellationToken).ConfigureAwait(false); + + if (packageQuickInfoAddresses == null || packageQuickInfoAddresses.Count == 0) + { + return null; + } + + string packageQuickInfoAddress = packageQuickInfoAddresses[0]; + + string location = packageQuickInfoAddress.Replace("{id-lower}", packageId.ToLowerInvariant()); + JObject responseJson; + + try + { + responseJson = await _webRequestFactory.GetJsonAsync(location, cancellationToken).ConfigureAwait(false) as JObject; + } + catch + { + return null; + } + + if (responseJson != null && responseJson.TryGetValue("items", out JToken topLevelItemsParseItem)) + { + JArray topLevelItemsArray = topLevelItemsParseItem as JArray; + JObject packageResultsContainer = topLevelItemsArray?.FirstOrDefault() as JObject; + JToken packageResultsItemsParseItem; + + if (packageResultsContainer != null && packageResultsContainer.TryGetValue("items", out packageResultsItemsParseItem)) + { + JArray packageResultsItems = packageResultsItemsParseItem as JArray; + + if (packageResultsItemsParseItem == null) + { + return null; + } + + string id, title, authors, summary, description, licenseUrl, projectUrl, iconUrl, tags; + SemanticVersion bestSemanticVersion = null; + PackageInfo packageInfo = null; + + foreach (JToken element in packageResultsItems) + { + JObject packageContainer = element as JObject; + JToken catalogEntryParseItem; + + if (packageContainer != null && packageContainer.TryGetValue("catalogEntry", out catalogEntryParseItem)) + { + JObject catalogEntry = catalogEntryParseItem as JObject; + + if (catalogEntry != null) + { + string ver = catalogEntry["version"]?.ToString(); + + if (ver == null) + { + continue; + } + + SemanticVersion currentVersion = SemanticVersion.Parse(ver); + + if (version != null) + { + if (string.Equals(version, catalogEntry["version"]?.ToString(), StringComparison.OrdinalIgnoreCase)) + { + id = catalogEntry["id"]?.ToString(); + title = catalogEntry ["title"]?.ToString (); + authors = catalogEntry["authors"]?.ToString(); + summary = catalogEntry ["summary"]?.ToString (); + description = catalogEntry["description"]?.ToString(); + projectUrl = catalogEntry["projectUrl"]?.ToString(); + licenseUrl = catalogEntry["licenseUrl"]?.ToString(); + iconUrl = catalogEntry["iconUrl"]?.ToString(); + tags = catalogEntry ["tags"]?.ToString (); + packageInfo = new PackageInfo(id, version, title, authors, summary, description, licenseUrl, projectUrl, iconUrl, tags, _kind, null); + return packageInfo; + } + } + else + { + if(currentVersion.CompareTo(bestSemanticVersion) > 0) + { + id = catalogEntry["id"]?.ToString(); + title = catalogEntry["title"]?.ToString (); + authors = catalogEntry["authors"]?.ToString(); + summary = catalogEntry["summary"]?.ToString(); + description = catalogEntry["description"]?.ToString (); + projectUrl = catalogEntry["projectUrl"]?.ToString(); + licenseUrl = catalogEntry["licenseUrl"]?.ToString(); + iconUrl = catalogEntry["iconUrl"]?.ToString (); + tags = catalogEntry["tags"]?.ToString(); + packageInfo = new PackageInfo(id, version, title, authors, summary, description, licenseUrl, projectUrl, iconUrl, tags, _kind, null); + bestSemanticVersion = currentVersion; + } + } + } + } + } + + return packageInfo; + } + } + + return null; + } + + public async Task GetPackageVersionsAsync(string id, IPackageQueryConfiguration queryConfiguration, CancellationToken cancellationToken) + { + IReadOnlyList results = new List(); + string frameworkQuery = !string.IsNullOrEmpty(queryConfiguration.CompatibilityTarget) ? $"&supportedFramework={queryConfiguration.CompatibilityTarget}" : ""; + const string autoCompleteServiceTypeIdentifier = "SearchAutocompleteService"; + List serviceEndpoints = await DiscoverEndpointsAsync(_feed, autoCompleteServiceTypeIdentifier, cancellationToken).ConfigureAwait(false); + Func queryFunc = x => $"{x}?id={id}{frameworkQuery}&take={queryConfiguration.MaxResults}&prerelease={queryConfiguration.IncludePreRelease}"; + JObject document = await ExecuteAutocompleteServiceQueryAsync(serviceEndpoints, queryFunc, cancellationToken).ConfigureAwait(false); + + try + { + results = GetDataFromNuGetV3CompatibleQueryResult(document); + } + catch + { + return PackageVersionSearchResult.Failure; + } + + return new PackageVersionSearchResult(results, _kind); + } + + private static IReadOnlyList GetDataFromNuGetV3CompatibleQueryResult(JObject document) + { + List results = new List(); + + if (document != null) + { + JArray resultsArray = document["data"] as JArray; + + if (resultsArray != null) + { + foreach (JToken curResult in resultsArray) + { + string curPackageId = curResult.ToString(); + results.Add(curPackageId); + } + } + } + + return results.AsReadOnly(); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/IO/FileSystem.cs b/MonoDevelop.MSBuild/PackageSearch/IO/FileSystem.cs new file mode 100644 index 00000000..71e246f8 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/IO/FileSystem.cs @@ -0,0 +1,68 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ProjectFileTools.NuGetSearch.IO; + +public class FileSystem : IFileSystem +{ + public bool DirectoryExists(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + // avoid a first-chance exception in Directory.Exists if we know it's a URL anyway + if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return path.IndexOfAny(Path.GetInvalidPathChars()) < 0 && Directory.Exists(path); + } + + public IEnumerable EnumerateDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (!DirectoryExists(path)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateDirectories(path, pattern, searchOption); + } + + public IEnumerable EnumerateFiles(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (!DirectoryExists(path)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateFiles(path, pattern, searchOption); + } + + public bool FileExists(string path) + { + return path.IndexOfAny(Path.GetInvalidPathChars()) < 0 && File.Exists(path); + } + + public string GetDirectoryName(string path) + { + return Path.GetDirectoryName(path); + } + + public string GetDirectoryNameOnly(string path) + { + return new DirectoryInfo(path).Name; + } + + public string ReadAllText(string path) + { + return File.ReadAllText(path); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/IO/IFileSystem.cs b/MonoDevelop.MSBuild/PackageSearch/IO/IFileSystem.cs new file mode 100644 index 00000000..54b444e2 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/IO/IFileSystem.cs @@ -0,0 +1,24 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.IO; + +namespace ProjectFileTools.NuGetSearch.IO; + +public interface IFileSystem +{ + IEnumerable EnumerateFiles(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly); + + IEnumerable EnumerateDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly); + + string ReadAllText(string path); + + bool DirectoryExists(string path); + + bool FileExists(string path); + + string GetDirectoryName(string path); + + string GetDirectoryNameOnly(string path); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/IO/IWebRequestFactory.cs b/MonoDevelop.MSBuild/PackageSearch/IO/IWebRequestFactory.cs new file mode 100644 index 00000000..4246280a --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/IO/IWebRequestFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectFileTools.NuGetSearch.IO; + +public interface IWebRequestFactory +{ + Task GetStringAsync(string endpoint, CancellationToken cancellationToken); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactory.cs b/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactory.cs new file mode 100644 index 00000000..bf9d4cee --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectFileTools.NuGetSearch.IO; + +public class WebRequestFactory : IWebRequestFactory +{ + private static readonly HttpClient _httpClient = new HttpClient(); + public async Task GetStringAsync(string endpoint, CancellationToken cancellationToken) + { + try + { + HttpResponseMessage responseMessage = await _httpClient.GetAsync(endpoint, cancellationToken).ConfigureAwait(false); + if (responseMessage.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // avoid a first-chance exception on 404 + return null; + } + + responseMessage.EnsureSuccessStatusCode(); + return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch + { + return null; + } + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactoryExtensions.cs b/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactoryExtensions.cs new file mode 100644 index 00000000..89439e79 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/IO/WebRequestFactoryExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace ProjectFileTools.NuGetSearch.IO; + +public static class WebRequestFactoryExtensions +{ + public static async Task GetJsonAsync(this IWebRequestFactory factory, string endpoint, CancellationToken cancellationToken) + { + string result = await factory.GetStringAsync(endpoint, cancellationToken).ConfigureAwait(false); + + if(result == null) + { + return null; + } + + try + { + return JToken.Parse(result); + } + catch + { + return null; + } + } + + public static async Task GetDeserializedJsonAsync(this IWebRequestFactory factory, string endpoint, CancellationToken cancellationToken) + { + JToken token = await factory.GetJsonAsync(endpoint, cancellationToken).ConfigureAwait(false); + + if(token == null) + { + return default(T); + } + + return token.ToObject(); + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/PackageSearchHelpers.cs b/MonoDevelop.MSBuild/PackageSearch/PackageSearchHelpers.cs index 7b829c05..5861b1c8 100644 --- a/MonoDevelop.MSBuild/PackageSearch/PackageSearchHelpers.cs +++ b/MonoDevelop.MSBuild/PackageSearch/PackageSearchHelpers.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; + using ProjectFileTools.NuGetSearch.Contracts; using ProjectFileTools.NuGetSearch.Feeds; diff --git a/MonoDevelop.MSBuild/PackageSearch/Search/PackageFeedSearchJob.cs b/MonoDevelop.MSBuild/PackageSearch/Search/PackageFeedSearchJob.cs new file mode 100644 index 00000000..29a1c9ea --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Search/PackageFeedSearchJob.cs @@ -0,0 +1,105 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; + +namespace ProjectFileTools.NuGetSearch.Search; + +internal class PackageFeedSearchJob : IPackageFeedSearchJob +{ + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly ConcurrentDictionary>, string> _taskMap = new ConcurrentDictionary>, string>(); + private readonly object _updateSync = new object(); + private readonly object _cancellationSync = new object(); + private bool _isInitializing; + + public event EventHandler Updated; + + public PackageFeedSearchJob(IReadOnlyList>>> searchTasks, CancellationTokenSource cancellationTokenSource) + { + _cancellationTokenSource = cancellationTokenSource; + List searchingIn = new List(searchTasks.Count); + _isInitializing = true; + Results = new List(); + + foreach (Tuple>> taskInfo in searchTasks) + { + _taskMap[taskInfo.Item2] = taskInfo.Item1; + taskInfo.Item2.ContinueWith(HandleCompletion); + searchingIn.Add(taskInfo.Item1); + } + + RemainingFeeds = searchingIn; + SearchingIn = searchingIn; + + _isInitializing = false; + foreach (Tuple>> taskInfo in searchTasks) + { + if (taskInfo.Item2.IsCompleted) + { + HandleCompletion(taskInfo.Item2); + } + } + } + + private void HandleCompletion(Task> task) + { + if (_isInitializing) + { + return; + } + + if (_taskMap.TryRemove(task, out string name)) + { + lock (_updateSync) + { + List remaining = RemainingFeeds.ToList(); + remaining.Remove(name); + RemainingFeeds = remaining; + + if (task.Result != null) + { + List results = Results.ToList(); + results.AddRange(task.Result.Where(x => !Equals(x, default(T)))); + Results = results; + } + } + + Updated?.Invoke(this, EventArgs.Empty); + } + } + + public void Cancel() + { + if(RemainingFeeds.Count == 0) + { + return; + } + + if (!_cancellationTokenSource.IsCancellationRequested) + { + lock (_cancellationSync) + { + if (!_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource.Cancel(); + IsCancelled = true; + } + } + } + } + + public IReadOnlyList RemainingFeeds { get; private set; } + + public IReadOnlyList Results { get; private set; } + + public IReadOnlyList SearchingIn { get; } + + public bool IsCancelled { get; private set; } +} \ No newline at end of file diff --git a/MonoDevelop.MSBuild/PackageSearch/Search/PackageNameSearchResult.cs b/MonoDevelop.MSBuild/PackageSearch/Search/PackageNameSearchResult.cs new file mode 100644 index 00000000..9d923369 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Search/PackageNameSearchResult.cs @@ -0,0 +1,37 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Search; + +internal class PackageNameSearchResult : IPackageNameSearchResult +{ + public static IPackageNameSearchResult Cancelled { get; } = new PackageNameSearchResult(); + + public static IPackageNameSearchResult Failure { get; } = new PackageNameSearchResult(); + + public static Task CancelledTask { get; } = Task.FromResult(Cancelled); + + public static Task FailureTask { get; } = Task.FromResult(Failure); + + public bool Success { get; } + + public IReadOnlyList Names { get; } + + public FeedKind SourceKind { get; } + + private PackageNameSearchResult() + { + } + + public PackageNameSearchResult(IReadOnlyList names, FeedKind sourceKind) + { + Success = true; + Names = names; + SourceKind = sourceKind; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Search/PackageSearchManager.cs b/MonoDevelop.MSBuild/PackageSearch/Search/PackageSearchManager.cs new file mode 100644 index 00000000..7411b6ae --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Search/PackageSearchManager.cs @@ -0,0 +1,255 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Search; + +public class PackageSearchManager : IPackageSearchManager +{ + private readonly IPackageFeedFactorySelector _factorySelector; + private readonly IPackageFeedRegistryProvider _feedRegistry; + private readonly ConcurrentDictionary>>> _cachedNameSearches; + private readonly ConcurrentDictionary>>> _cachedVersionSearches; + + public PackageSearchManager(IPackageFeedRegistryProvider feedRegistry, IPackageFeedFactorySelector factorySelector) + { + _feedRegistry = feedRegistry; + _factorySelector = factorySelector; + _cachedNameSearches = new ConcurrentDictionary>>>(); + _cachedVersionSearches = new ConcurrentDictionary>>>(); + } + + private class PackageNameQuery + { + private readonly int _hashCode; + private readonly string? _prefix; + private readonly string? _tfm; + private readonly PackageType? _packageType; + + public PackageNameQuery(string? prefix, string? tfm, PackageType? packageType) + { + _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(prefix ?? "") + ^ (tfm?.GetHashCode() ?? 0) + ^ (packageType?.GetHashCode() ?? 0); + + _prefix = prefix; + _tfm = tfm; + _packageType = packageType; + } + + public override int GetHashCode() + { + return _hashCode; + } + + public override bool Equals(object? obj) => + obj is PackageNameQuery q + && q._hashCode == _hashCode + && string.Equals(_prefix, q._prefix, StringComparison.Ordinal) + && string.Equals(_tfm, q._tfm, StringComparison.Ordinal) + && (_packageType?.Equals(q._packageType) ?? false); + } + + private class PackageVersionQuery + { + private readonly int _hashCode; + private readonly string _packageName; + private readonly string? _tfm; + + public PackageVersionQuery (string packageName, string? tfm) + { + _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(packageName) ^ (tfm?.GetHashCode() ?? 0); + _packageName = packageName; + _tfm = tfm; + } + + public override int GetHashCode() + { + return _hashCode; + } + + public override bool Equals(object? obj) => + obj is PackageVersionQuery q + && q._hashCode == _hashCode + && string.Equals(_packageName, q._packageName, StringComparison.Ordinal) + && string.Equals(_tfm, q._tfm, StringComparison.Ordinal); + } + + public IPackageFeedSearchJob> SearchPackageNames(string prefix, string? tfm, string? packageType = null) + { + var packageTypeObj = packageType != null? new PackageType(packageType, null) : null; + var config = new PackageQueryConfiguration(tfm, packageType: packageTypeObj); + + var bag = _cachedNameSearches.GetOrAdd(config, x => new ConcurrentDictionary>>()); + return bag.AddOrUpdate(new PackageNameQuery(prefix, tfm, packageTypeObj), x => SearchPackageNamesInternal(prefix, config), (x, e) => + { + if (e.IsCancelled) + { + return SearchPackageNamesInternal(prefix, config); + } + + return e; + }); + } + + private IPackageFeedSearchJob> SearchPackageNamesInternal(string prefix, IPackageQueryConfiguration config) + { + var searchTasks = new List>>>>(); + var cancellationTokenSource = new CancellationTokenSource(); + CancellationToken cancellationToken = cancellationTokenSource.Token; + + foreach (string feedSource in _feedRegistry.ConfiguredFeeds) + { + IPackageFeed feed = _factorySelector.GetFeed(feedSource); + + if (feed != null) + { + searchTasks.Add(new Tuple>>>(feed.DisplayName, feed.GetPackageNamesAsync(prefix, config, cancellationToken).ContinueWith(TransformToPackageInfo))); + } + } + + return new PackageFeedSearchJob>(searchTasks, cancellationTokenSource); + } + + private IReadOnlyList> TransformToPackageInfo(Task arg) + { + if (arg.IsFaulted) + { + throw arg.Exception; + } + + if (arg.IsCanceled) + { + throw new TaskCanceledException(); + } + + List> packages = new List>(); + + if (arg.Result?.Success ?? false) + { + foreach (string name in arg.Result.Names) + { + Tuple result = Tuple.Create(name, arg.Result.SourceKind); + packages.Add(result); + } + } + + return packages; + } + + public IPackageFeedSearchJob> SearchPackageVersions(string packageName, string? tfm, string? packageType = null) + { + var packageTypeObj = packageType != null ? new PackageType (packageType, null) : null; + var config = new PackageQueryConfiguration (tfm, packageType: packageTypeObj); + + var bag = _cachedVersionSearches.GetOrAdd(config, x => new ConcurrentDictionary>>()); + return bag.AddOrUpdate(new PackageVersionQuery(packageName, tfm), x => SearchPackageVersionsInternal(packageName, tfm, config), (x, e) => + { + if (e.IsCancelled) + { + return SearchPackageVersionsInternal(packageName, tfm, config); + } + + return e; + }); + } + + public IPackageFeedSearchJob> SearchPackageVersionsInternal(string packageName, string? tfm, IPackageQueryConfiguration config) + { + List>>>> searchTasks = new List>>>>(); + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + CancellationToken cancellationToken = cancellationTokenSource.Token; + + foreach (string feedSource in _feedRegistry.ConfiguredFeeds) + { + IPackageFeed feed = _factorySelector.GetFeed(feedSource); + + if (feed != null) + { + searchTasks.Add(new Tuple>>>(feed.DisplayName, feed.GetPackageVersionsAsync(packageName, config, cancellationToken).ContinueWith(TransformToPackageVersion))); + } + } + + return new PackageFeedSearchJob>(searchTasks, cancellationTokenSource); + } + + private IReadOnlyList> TransformToPackageVersion(Task arg) + { + if (arg.IsFaulted) + { + throw arg.Exception; + } + + if (arg.IsCanceled) + { + throw new TaskCanceledException(); + } + + if (arg.Result?.Success ?? false) + { + List> results = new List>(); + + foreach(string ver in arg.Result.Versions) + { + results.Add(Tuple.Create(ver, arg.Result.SourceKind)); + } + + return results; + } + + return new List>(); + } + + public IPackageFeedSearchJob SearchPackageInfo(string packageId, string? version, string? tfm) + { + ConcurrentDictionary> lookup = PackageInfoLookup.GetOrAdd(packageId, id => new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase)); + return lookup.AddOrUpdate(version ?? string.Empty, ver => Compute(packageId, version, tfm), (ver, e) => + { + if (e.IsCancelled) + { + return Compute(packageId, version, tfm); + } + + return e; + }); + } + + private IPackageFeedSearchJob Compute(string packageId, string? version, string? tfm) + { + IPackageQueryConfiguration config = new PackageQueryConfiguration(tfm); + List>>> searchTasks = new List>>>(); + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + CancellationToken cancellationToken = cancellationTokenSource.Token; + + foreach (string feedSource in _feedRegistry.ConfiguredFeeds) + { + IPackageFeed feed = _factorySelector.GetFeed(feedSource); + + if (feed != null) + { + searchTasks.Add(Tuple.Create(feedSource, feed.GetPackageInfoAsync(packageId, version, config, cancellationToken).ContinueWith(x => + { + if (x == null || x.IsFaulted || x.IsCanceled) + { + return new IPackageInfo[0]; + } + + return (IReadOnlyList)new[] { x.Result }; + }))); + } + } + + return new PackageFeedSearchJob(searchTasks, cancellationTokenSource); + } + + private static readonly ConcurrentDictionary>> PackageInfoLookup = new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); +} diff --git a/MonoDevelop.MSBuild/PackageSearch/Search/PackageVersionSearchResult.cs b/MonoDevelop.MSBuild/PackageSearch/Search/PackageVersionSearchResult.cs new file mode 100644 index 00000000..02bd4336 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/Search/PackageVersionSearchResult.cs @@ -0,0 +1,37 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using ProjectFileTools.NuGetSearch.Contracts; +using ProjectFileTools.NuGetSearch.Feeds; + +namespace ProjectFileTools.NuGetSearch.Search; + +internal class PackageVersionSearchResult : IPackageVersionSearchResult +{ + public static IPackageVersionSearchResult Cancelled { get; } = new PackageVersionSearchResult(); + + public static Task CancelledTask { get; } = Task.FromResult(Cancelled); + + public static IPackageVersionSearchResult Failure { get; } = new PackageVersionSearchResult(); + + public static Task FailureTask { get; } = Task.FromResult(Failure); + + public bool Success { get; } + + public IReadOnlyList Versions { get; } + + public FeedKind SourceKind { get; } + + private PackageVersionSearchResult() + { + } + + public PackageVersionSearchResult(IReadOnlyList versions, FeedKind kind) + { + Versions = versions; + Success = true; + SourceKind = kind; + } +} diff --git a/MonoDevelop.MSBuild/PackageSearch/SemanticVersion.cs b/MonoDevelop.MSBuild/PackageSearch/SemanticVersion.cs new file mode 100644 index 00000000..1a5289e3 --- /dev/null +++ b/MonoDevelop.MSBuild/PackageSearch/SemanticVersion.cs @@ -0,0 +1,193 @@ +// Copyright (c).NET Foundation and Contributors +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace ProjectFileTools.NuGetSearch; + +/// +/// Semantic version 2.0.0 parser per http://semver.org/spec/v2.0.0.html +/// +public class SemanticVersion : IComparable, IEquatable +{ + private readonly int _hashCode; + + public int Major { get; private set; } + + public int Minor { get; private set; } + + public int Patch { get; private set; } + + public string BuildMetadata { get; private set; } + + public string PrereleaseVersion { get; private set; } + + public string OriginalText { get; private set; } + + private SemanticVersion(string originalText) + { + _hashCode = originalText?.GetHashCode() ?? 0; + OriginalText = originalText; + } + + public static SemanticVersion Parse(string value) + { + SemanticVersion ver = new SemanticVersion(value); + + if (value == null) + { + return ver; + } + + int prereleaseStart = value.IndexOf('-'); + int buildMetadataStart = value.IndexOf('+'); + + //If the index of the build metadata marker (+) is greater than the index of the prerelease marker (-) + // then it is necessarily found in the string because if both were not found they'd be equal + if (buildMetadataStart > prereleaseStart) + { + //If the build metadata marker is not the last character in the string, take off everything after it + // and use it for the build metadata field + if (buildMetadataStart < value.Length - 1) + { + ver.BuildMetadata = value.Substring(buildMetadataStart + 1); + } + + value = value.Substring(0, buildMetadataStart); + + //If the prerelease section is found, extract it + if (prereleaseStart > -1) + { + //If the prerelease section marker is not the last character in the string, take off everything after it + // and use it for the prerelease field + if (prereleaseStart < value.Length - 1) + { + ver.PrereleaseVersion = value.Substring(prereleaseStart + 1); + } + + value = value.Substring(0, prereleaseStart); + } + } + //If the build metadata wasn't the last metadata section found, check to see if a prerelease section exists. + // If it doesn't, then neither section exists + else if (prereleaseStart > -1) + { + //If the prerelease version marker is not the last character in the string, take off everything after it + // and use it for the prerelease version field + if (prereleaseStart < value.Length - 1) + { + ver.PrereleaseVersion = value.Substring(prereleaseStart + 1); + } + + value = value.Substring(0, prereleaseStart); + + //If the build metadata section is found, extract it + if (buildMetadataStart > -1) + { + //If the build metadata marker is not the last character in the string, take off everything after it + // and use it for the build metadata field + if (buildMetadataStart < value.Length - 1) + { + ver.BuildMetadata = value.Substring(buildMetadataStart + 1); + } + + value = value.Substring(0, buildMetadataStart); + } + } + + string[] versionParts = value.Split('.'); + + if (versionParts.Length > 0) + { + int major; + int.TryParse(versionParts[0], out major); + ver.Major = major; + } + + if (versionParts.Length > 1) + { + int minor; + int.TryParse(versionParts[1], out minor); + ver.Minor = minor; + } + + if (versionParts.Length > 2) + { + int patch; + int.TryParse(versionParts[2], out patch); + ver.Patch = patch; + } + + return ver; + } + + public int CompareTo(SemanticVersion other) + { + if (other == null) + { + return 1; + } + + int result = Major.CompareTo(other.Major); + + if (result != 0) + { + return result; + } + + result = Minor.CompareTo(other.Minor); + + if (result != 0) + { + return result; + } + + result = Patch.CompareTo(other.Patch); + + if (result != 0) + { + return result; + } + + //A version not marked with prerelease is later than one with a prerelease designation + if (PrereleaseVersion == null && other.PrereleaseVersion != null) + { + return 1; + } + + //A version not marked with prerelease is later than one with a prerelease designation + if (PrereleaseVersion != null && other.PrereleaseVersion == null) + { + return -1; + } + + result = StringComparer.OrdinalIgnoreCase.Compare(PrereleaseVersion, other.PrereleaseVersion); + + if (result != 0) + { + return result; + } + + return StringComparer.OrdinalIgnoreCase.Compare(OriginalText, other.OriginalText); + } + + public bool Equals(SemanticVersion other) + { + return other != null && string.Equals(OriginalText, other.OriginalText, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + return Equals(obj as SemanticVersion); + } + + public override int GetHashCode() + { + return _hashCode; + } + + public override string ToString() + { + return OriginalText; + } +} diff --git a/MonoDevelop.MSBuild/Schema/MSBuildCompletionExtensions.cs b/MonoDevelop.MSBuild/Schema/MSBuildCompletionExtensions.cs index b4714c0a..2e913d92 100644 --- a/MonoDevelop.MSBuild/Schema/MSBuildCompletionExtensions.cs +++ b/MonoDevelop.MSBuild/Schema/MSBuildCompletionExtensions.cs @@ -94,7 +94,7 @@ public static bool IsInTarget (this MSBuildElementSyntax resolvedElement, XEleme } public static IEnumerable GetElementCompletions (this IEnumerable schemas, - MSBuildElementSyntax languageElement, string elementName) + MSBuildElementSyntax? languageElement, string? elementName) { if (languageElement == null) { yield return MSBuildElementSyntax.Project; @@ -105,6 +105,10 @@ public static IEnumerable GetElementCompletions (this IEnumerable GetElementCompletions (this IEnumerable GetAbstractChildren (this IEnumerable schemas, - MSBuildElementSyntax languageElement, string elementName) + MSBuildElementSyntax? languageElement, string? elementName) { if (languageElement == null) { yield return MSBuildElementSyntax.Project; @@ -131,6 +135,10 @@ public static IEnumerable GetAbstractChildren (this IEnumerable paths) + public SdkInfo (string name, string? version, IList paths) { Name = name ?? throw new ArgumentNullException (nameof (name)); Version = version; @@ -52,7 +52,7 @@ public SdkInfo (string name, string version, IList paths) } public string Name { get; } - public string Version { get; } + public string? Version { get; } /// /// The SDK path(s). May be empty e.g. for WorkloadAutoImportPropsLocator, but will not be null. diff --git a/MonoDevelop.MSBuild/Util/TextWithMarkers.cs b/MonoDevelop.MSBuild/Util/TextWithMarkers.cs deleted file mode 100644 index 5e7ed9f4..00000000 --- a/MonoDevelop.MSBuild/Util/TextWithMarkers.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Text; - -using MonoDevelop.Xml.Dom; - -namespace MonoDevelop.MSBuild.Util -{ - /// - /// Represents text with various marked positions. - /// - class TextWithMarkers - { - TextWithMarkers (string text, char[] markerChars, List[] markedPositionsById) - { - Text = text; - this.markerChars = markerChars; - this.markedPositionsById = markedPositionsById; - } - - readonly char[] markerChars; - readonly List[] markedPositionsById; - - /// - /// The text with the marker characters removed - /// - public string Text { get; } - - int GetMarkerId (char? markerChar) - { - int markerId; - if (markerChar is null) { - if (markedPositionsById.Length != 1) { - throw new ArgumentException ("More than one marker char was used in this document, you must specify which one", nameof (markerChar)); - } - markerId = 0; - } else { - markerId = Array.IndexOf (markerChars, markerChar); - if (markerId < 0) { - throw new ArgumentException ($"The character '{markerChar}' was not used as a marker", nameof (markerChar)); - } - } - return markerId; - } - - /// - /// Gets all the marked positions for the specified marker character (optional if only one was used). - /// - public IList GetMarkedPositions (char? markerChar = null) => markedPositionsById[GetMarkerId (markerChar)]; - - public int GetMarkedPosition (char? markerChar = null) - { - var id = GetMarkerId (markerChar); - var positions = markedPositionsById[id]; - - if (positions.Count != 1) { - throw new ArgumentException ($"Found multiple markers for char '{markerChars[id]}'", nameof (markerChar)); - } - - return positions[0]; - } - - public TextSpan GetMarkedSpan (char? markerChar = null) - { - var id = GetMarkerId (markerChar); - var positions = markedPositionsById[id]; - - // treat single marker as zero width span - if (positions.Count == 1) { - int pos = positions[0]; - return TextSpan.FromBounds (pos, pos); - } - - if (positions.Count == 2) { - int start = positions[0]; - int end = positions[1]; - return TextSpan.FromBounds (start, end); - } - - throw new ArgumentException ($"Found {positions.Count} markers for char '{markerChars[id]}', must have exactly 1 or 2 markers to treat as a span", nameof (markerChar)); - } - - public TextSpan[] GetMarkedSpans (char? markerChar = null) - { - var id = GetMarkerId (markerChar); - var markers = markedPositionsById[id]; - - if (markers.Count % 2 != 0) { - throw new ArgumentException ($"Found {markers.Count} markers for char '{markerChars[id]}', must have even number to treat as spans", nameof (markerChar)); - } - - var spans = new TextSpan [markers.Count / 2]; - - for (int i = 0; i < spans.Length; i++) { - int j = i * 2; - int start = markers[j]; - int end = markers[j + 1]; - spans[i] = TextSpan.FromBounds (start, end); - } - - return spans; - } - - public static TextWithMarkers Parse (string textWithMarkers, params char[] markerChars) - { - var markerIndices = Array.ConvertAll (markerChars, c => new List ()); - - var sb = new StringBuilder (textWithMarkers.Length); - - for (int i = 0; i < textWithMarkers.Length; i++) { - var c = textWithMarkers[i]; - int markerId = Array.IndexOf (markerChars, c); - if (markerId > -1) { - markerIndices[markerId].Add (sb.Length); - } else { - sb.Append (c); - } - } - - return new (sb.ToString (), markerChars, markerIndices); - } - - public static TextWithMarkers Parse (string textWithMarkers, char markerChar) - { - var markerIndices = new List (); - - var sb = new StringBuilder (textWithMarkers.Length); - - for (int i = 0; i < textWithMarkers.Length; i++) { - var c = textWithMarkers[i]; - if (c == markerChar) { - markerIndices.Add (sb.Length); - } else { - sb.Append (c); - } - } - - return new (sb.ToString (), new[] { markerChar }, new[] { markerIndices }); - } - } -} diff --git a/MonoDevelop.MSBuildEditor.sln b/MonoDevelop.MSBuildEditor.sln index 766b25ec..8fda1019 100644 --- a/MonoDevelop.MSBuildEditor.sln +++ b/MonoDevelop.MSBuildEditor.sln @@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props + NuGet.Config = NuGet.Config README.md = README.md TODO.md = TODO.md EndProjectSection @@ -23,8 +24,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoDevelop.MSBuild.Editor" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoDevelop.MSBuild.Editor.VisualStudio", "MonoDevelop.MSBuild.Editor.VisualStudio\MonoDevelop.MSBuild.Editor.VisualStudio.csproj", "{6D7BB05D-5C0A-4A4E-A177-43F5AF67DF5E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.MiniEditor", "MonoDevelop.Xml\external\MiniEditor\Microsoft.VisualStudio.MiniEditor\Microsoft.VisualStudio.MiniEditor.csproj", "{BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoDevelop.MSBuild.Tests.Editor", "MonoDevelop.MSBuild.Tests.Editor\MonoDevelop.MSBuild.Tests.Editor.csproj", "{9ADE035C-CBF3-4FE3-B01F-D6844FADCAD3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoDevelop.Xml.Core.Tests", "MonoDevelop.Xml\Core.Tests\MonoDevelop.Xml.Core.Tests.csproj", "{0B54E9C0-AADE-4515-B731-D244586D6BD1}" @@ -39,11 +38,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MonoDevelop.Xml", "MonoDeve EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XsdSchemaImporter", "XsdSchemaImporter\XsdSchemaImporter.csproj", "{36BACB3E-74B0-45D8-9C48-178BB9549BF1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MiniEditor", "MiniEditor", "{7D15BAC9-838F-4908-B9DB-CDFDF5AF66D2}" - ProjectSection(SolutionItems) = preProject - MonoDevelop.Xml\external\MiniEditor\Directory.Build.props = MonoDevelop.Xml\external\MiniEditor\Directory.Build.props - MonoDevelop.Xml\external\MiniEditor\Directory.Packages.props = MonoDevelop.Xml\external\MiniEditor\Directory.Packages.props - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuildLanguageServer", "MSBuildLanguageServer\MSBuildLanguageServer.csproj", "{6D59750D-6CF5-423D-9788-C5733A5BB662}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuildLanguageServer.Tests", "MSBuildLanguageServer.Tests\MSBuildLanguageServer.Tests.csproj", "{0B8A7A95-E87C-4C81-97BC-28BABA5884A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoDevelop.MSBuild.Editor.Common", "MonoDevelop.MSBuild.Editor.Common\MonoDevelop.MSBuild.Editor.Common.csproj", "{ACA083DA-1A7E-4D8B-B39E-EE93A1881168}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,14 +98,6 @@ Global {6D7BB05D-5C0A-4A4E-A177-43F5AF67DF5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D7BB05D-5C0A-4A4E-A177-43F5AF67DF5E}.Release|Any CPU.Build.0 = Release|Any CPU {6D7BB05D-5C0A-4A4E-A177-43F5AF67DF5E}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.DebugMac|Any CPU.Build.0 = Debug|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.Release|Any CPU.Build.0 = Release|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU {9ADE035C-CBF3-4FE3-B01F-D6844FADCAD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9ADE035C-CBF3-4FE3-B01F-D6844FADCAD3}.Debug|Any CPU.Build.0 = Debug|Any CPU {9ADE035C-CBF3-4FE3-B01F-D6844FADCAD3}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU @@ -139,21 +130,47 @@ Global {36BACB3E-74B0-45D8-9C48-178BB9549BF1}.Release|Any CPU.Build.0 = Release|Any CPU {36BACB3E-74B0-45D8-9C48-178BB9549BF1}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU {36BACB3E-74B0-45D8-9C48-178BB9549BF1}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.DebugMac|Any CPU.Build.0 = Debug|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.Release|Any CPU.Build.0 = Release|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.ReleaseMac|Any CPU.ActiveCfg = Debug|Any CPU + {6D59750D-6CF5-423D-9788-C5733A5BB662}.ReleaseMac|Any CPU.Build.0 = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.DebugMac|Any CPU.Build.0 = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.Release|Any CPU.Build.0 = Release|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.ReleaseMac|Any CPU.ActiveCfg = Debug|Any CPU + {0B8A7A95-E87C-4C81-97BC-28BABA5884A9}.ReleaseMac|Any CPU.Build.0 = Debug|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.DebugMac|Any CPU.ActiveCfg = Debug|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.DebugMac|Any CPU.Build.0 = Debug|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.Release|Any CPU.Build.0 = Release|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.ReleaseMac|Any CPU.ActiveCfg = Release|Any CPU + {ACA083DA-1A7E-4D8B-B39E-EE93A1881168}.ReleaseMac|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {7D15BAC9-838F-4908-B9DB-CDFDF5AF66D2} = {442FCBDB-C87E-4BA4-BA76-463F7877570F} {87DE05FC-4B18-4C21-8AA5-237CB5B97780} = {442FCBDB-C87E-4BA4-BA76-463F7877570F} {563FFDF7-0739-42DF-B987-B804A26D1E0B} = {442FCBDB-C87E-4BA4-BA76-463F7877570F} - {BE0CC91A-31C5-4C0D-B2E4-E8D781DCD912} = {7D15BAC9-838F-4908-B9DB-CDFDF5AF66D2} {0B54E9C0-AADE-4515-B731-D244586D6BD1} = {442FCBDB-C87E-4BA4-BA76-463F7877570F} {BF75E67F-34E4-412C-B886-E4251366EE57} = {442FCBDB-C87E-4BA4-BA76-463F7877570F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7D111E12-470F-4E1D-9A39-DB2611E338C7} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + external\roslyn\src\Dependencies\Collections\Microsoft.CodeAnalysis.Collections.projitems*{aca083da-1a7e-4d8b-b39e-ee93a1881168}*SharedItemsImports = 5 + external\roslyn\src\Dependencies\PooledObjects\Microsoft.CodeAnalysis.PooledObjects.projitems*{aca083da-1a7e-4d8b-b39e-ee93a1881168}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.TextStylePolicy = $3 diff --git a/MonoDevelop.MSBuildEditor/MonoDevelop.MSBuildEditor.csproj b/MonoDevelop.MSBuildEditor/MonoDevelop.MSBuildEditor.csproj index 4092907d..c33e0acf 100644 --- a/MonoDevelop.MSBuildEditor/MonoDevelop.MSBuildEditor.csproj +++ b/MonoDevelop.MSBuildEditor/MonoDevelop.MSBuildEditor.csproj @@ -32,10 +32,10 @@ - + - + diff --git a/MonoDevelop.Xml b/MonoDevelop.Xml index 26e00743..e5c608d1 160000 --- a/MonoDevelop.Xml +++ b/MonoDevelop.Xml @@ -1 +1 @@ -Subproject commit 26e00743925f42c0dcc78dedc0121c662b08a126 +Subproject commit e5c608d10e052e970a80174c0a672ba92cf0decc diff --git a/NoVSEditor.slnf b/NoVSEditor.slnf new file mode 100644 index 00000000..389e5fc7 --- /dev/null +++ b/NoVSEditor.slnf @@ -0,0 +1,15 @@ +{ + "solution": { + "path": "MonoDevelop.MSBuildEditor.sln", + "projects": [ + "MSBuildLanguageServer.Tests\\MSBuildLanguageServer.Tests.csproj", + "MSBuildLanguageServer\\MSBuildLanguageServer.csproj", + "MonoDevelop.MSBuild.Editor.Common\\MonoDevelop.MSBuild.Editor.Common.csproj", + "MonoDevelop.MSBuild.Tests\\MonoDevelop.MSBuild.Tests.csproj", + "MonoDevelop.MSBuild\\MonoDevelop.MSBuild.csproj", + "MonoDevelop.Xml\\Core.Tests\\MonoDevelop.Xml.Core.Tests.csproj", + "MonoDevelop.Xml\\Core\\MonoDevelop.Xml.Core.csproj", + "XsdSchemaImporter\\XsdSchemaImporter.csproj" + ] + } +} \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config index ca1f40b2..d82dd762 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,6 +1,23 @@ - + - - - + + + + + + + + + + + + + + + + + + + + diff --git a/XsdSchemaImporter/MSBuildXsdSchemaReader.cs b/XsdSchemaImporter/MSBuildXsdSchemaReader.cs index 32ec6a2a..88181104 100644 --- a/XsdSchemaImporter/MSBuildXsdSchemaReader.cs +++ b/XsdSchemaImporter/MSBuildXsdSchemaReader.cs @@ -291,7 +291,7 @@ static bool IsMetadataConditionAttribute(XmlSchemaComplexType complexType) => //TODO: read parameters - return new TaskInfo(name, docMarkup, TaskDeclarationKind.Inferred, null, null, null, null, 0, null); + return new TaskInfo(name, docMarkup, TaskDeclarationKind.Inferred, null, null, null, null, null, null); } IEnumerable CheckCollection(XmlSchemaObject parent, XmlSchemaObjectCollection collection) where T : XmlSchemaObject diff --git a/external/ProjFileTools b/external/ProjFileTools deleted file mode 160000 index 6f4e5c5b..00000000 --- a/external/ProjFileTools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f4e5c5b8d8a9906c3c9b9b64a70440863a749d2 diff --git a/external/roslyn b/external/roslyn new file mode 160000 index 00000000..c6a0795c --- /dev/null +++ b/external/roslyn @@ -0,0 +1 @@ +Subproject commit c6a0795ce110a904bf7d706a70335f7579390833 diff --git a/msbuild-editor-vscode/.editorconfig b/msbuild-editor-vscode/.editorconfig new file mode 100644 index 00000000..cde209ac --- /dev/null +++ b/msbuild-editor-vscode/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# Typescript files +[*.ts] +indent_style = tab +indent_size = 4 + +# Preserve formatting of files from extension template for now +[tsconfig.json, webpack.config.json] +indent_style = tab \ No newline at end of file diff --git a/msbuild-editor-vscode/.eslintrc.json b/msbuild-editor-vscode/.eslintrc.json new file mode 100644 index 00000000..8c61488b --- /dev/null +++ b/msbuild-editor-vscode/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "import", + "format": [ "camelCase", "PascalCase" ] + } + ], + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": [ + "out", + "dist", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/msbuild-editor-vscode/.vscode-test.mjs b/msbuild-editor-vscode/.vscode-test.mjs new file mode 100644 index 00000000..b62ba25f --- /dev/null +++ b/msbuild-editor-vscode/.vscode-test.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out/test/**/*.test.js', +}); diff --git a/msbuild-editor-vscode/.vscodeignore b/msbuild-editor-vscode/.vscodeignore new file mode 100644 index 00000000..4d88d30e --- /dev/null +++ b/msbuild-editor-vscode/.vscodeignore @@ -0,0 +1,14 @@ +.vscode/** +.vscode-test/** +out/** +node_modules/** +src/** +.gitignore +.yarnrc +webpack.config.js +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts +**/.vscode-test.* \ No newline at end of file diff --git a/msbuild-editor-vscode/CHANGELOG.md b/msbuild-editor-vscode/CHANGELOG.md new file mode 100644 index 00000000..ee76ea46 --- /dev/null +++ b/msbuild-editor-vscode/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "msbuild-editor" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/msbuild-editor-vscode/README.md b/msbuild-editor-vscode/README.md new file mode 100644 index 00000000..18a85752 --- /dev/null +++ b/msbuild-editor-vscode/README.md @@ -0,0 +1,71 @@ +# msbuild-editor README + +This is the README for your extension "msbuild-editor". After writing up a brief description, we recommend including the following sections. + +## Features + +Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. + +For example if there is an image subfolder under your extension project workspace: + +\!\[feature X\]\(images/feature-x.png\) + +> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. + +## Requirements + +If you have any requirements or dependencies, add a section describing those and how to install and configure them. + +## Extension Settings + +Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. + +For example: + +This extension contributes the following settings: + +* `myExtension.enable`: Enable/disable this extension. +* `myExtension.thing`: Set to `blah` to do something. + +## Known Issues + +Calling out known issues can help limit users opening duplicate issues against your extension. + +## Release Notes + +Users appreciate release notes as you update your extension. + +### 1.0.0 + +Initial release of ... + +### 1.0.1 + +Fixed issue #. + +### 1.1.0 + +Added features X, Y, and Z. + +--- + +## Following extension guidelines + +Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. + +* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) + +## Working with Markdown + +You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: + +* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). +* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). +* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. + +## For more information + +* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) +* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) + +**Enjoy!** diff --git a/msbuild-editor-vscode/esbuild.js b/msbuild-editor-vscode/esbuild.js new file mode 100644 index 00000000..cc2be598 --- /dev/null +++ b/msbuild-editor-vscode/esbuild.js @@ -0,0 +1,56 @@ +const esbuild = require("esbuild"); + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +/** + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + + setup(build) { + build.onStart(() => { + console.log('[watch] build started'); + }); + build.onEnd((result) => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + console.error(` ${location.file}:${location.line}:${location.column}:`); + }); + console.log('[watch] build finished'); + }); + }, +}; + +async function main() { + const ctx = await esbuild.context({ + entryPoints: [ + 'src/extension.ts' + ], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/extension.js', + external: ['vscode'], + logLevel: 'silent', + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin, + ], + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/msbuild-editor-vscode/language-configuration.json b/msbuild-editor-vscode/language-configuration.json new file mode 100644 index 00000000..31fc4a0e --- /dev/null +++ b/msbuild-editor-vscode/language-configuration.json @@ -0,0 +1,31 @@ +{ + "comments": { + "blockComment": [ "" ] + }, + // symbols used as brackets + "brackets": [ + ["<", ">"], + ["[", "]"], + ["(", ")"], + ["$(", ")"], + ["@(", ")"], + ["%(", ")"] + ], + // symbols that are auto closed when typing + "autoClosingPairs": [ + ["<", ">"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"], + ["$(", ")"], + ["@(", ")"], + ["%(", ")"] + ], + // symbols that can be used to surround a selection + "surroundingPairs": [ + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ] +} \ No newline at end of file diff --git a/msbuild-editor-vscode/package-lock.json b/msbuild-editor-vscode/package-lock.json new file mode 100644 index 00000000..6e69946d --- /dev/null +++ b/msbuild-editor-vscode/package-lock.json @@ -0,0 +1,5421 @@ +{ + "name": "msbuild-editor", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "msbuild-editor", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@vscode/extension-telemetry": "^0.9.0", + "rxjs": "6.6.7", + "semver": "7.5.4", + "uuid": "^9.0.0", + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^18.19.33", + "@types/semver": "7.3.13", + "@types/uuid": "^9.0.1", + "@types/vscode": "^1.89.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "@vscode/test-cli": "^0.0.9", + "@vscode/test-electron": "^2.3.9", + "esbuild": "^0.20.2", + "eslint": "^8.57.0", + "npm-run-all": "^4.1.5", + "typescript": "^5.4.5" + }, + "engines": { + "vscode": "^1.89.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/1ds-core-js": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.2.1.tgz", + "integrity": "sha512-fcowL6s42sj4+yU0nko9gkGDCGbeQEGgYxXnwejSAjMw9MKZ0jjYRvxFBR+Aip6aCzf0WV0Jw0j4FA72EXMbIg==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.2.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.1 < 2.x", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + } + }, + "node_modules/@microsoft/1ds-post-js": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.2.1.tgz", + "integrity": "sha512-MeGvTxBbADo8um8ylTJST+gDcIpKx0mLtLtcbJtgSdL+SerITf2wvweobE3/NMQiWI8URtCb1XIYKauW7lqVlg==", + "dependencies": { + "@microsoft/1ds-core-js": "4.2.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.1 < 2.x", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.2.1.tgz", + "integrity": "sha512-+aWBBbIW4/Tf4sLGZmWhd5chktBpKQpnCbkuoTHGe+AWO8Q8fsDa4w2Y89OGuEg9OJ3kr2VKTUU7LgILKFz/cg==", + "dependencies": { + "@microsoft/applicationinsights-common": "3.2.1", + "@microsoft/applicationinsights-core-js": "3.2.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.1 < 2.x", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.2.1.tgz", + "integrity": "sha512-vRYQ1SIZJEz1eFbs2AQiLtev5L+zmjZ1Jkj3BWfIxJLd6n0cVR4NZETBSyMuk11KH7MIOrDLvh1CzjBIJIpDAg==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.2.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.2.1.tgz", + "integrity": "sha512-euxkDrF5BroAY7wgviaTVZdMvRAENQtUW4pDTsIjJK26shi1m5fPCc5l+vMn7kO2wQEaEgAOVw+/kSQgXDHN+Q==", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.1 < 2.x", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.2.1.tgz", + "integrity": "sha512-aPvZX8VOSwLwnXpLpRnFXUB5Znw+9mcsp4/UMWi6V0SVGKTlyGEDn/xcHAN2uKEcb5aD/w9UYdAsbPWEW6yEpw==", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "3.2.1", + "@microsoft/applicationinsights-common": "3.2.1", + "@microsoft/applicationinsights-core-js": "3.2.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.1 < 2.x", + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.1.tgz", + "integrity": "sha512-O2kN8n2HpDWJ7Oji+oTMnhITrCndmrNvrHbGDwAIBydx+FWvLE/vrw4QwnRRMvSCa2AJrcP59Ryklxv30KfkWQ==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.11.2 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.2.tgz", + "integrity": "sha512-80W8BkS09kkGuUHJX50Fqq+QqAslxUaOQytH+3JhRacXs1EpEt2JOOkYKytqFZAYir3SeH9fahniEaDzIBxlUw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", + "integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vscode/extension-telemetry": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.6.tgz", + "integrity": "sha512-qWK2GNw+b69QRYpjuNM9g3JKToMICoNIdc0rQMtvb4gIG9vKKCZCVCz+ZOx6XM/YlfWAyuPiyxcjIY0xyF+Djg==", + "dependencies": { + "@microsoft/1ds-core-js": "^4.1.2", + "@microsoft/1ds-post-js": "^4.1.2", + "@microsoft/applicationinsights-web-basic": "^3.1.2" + }, + "engines": { + "vscode": "^1.75.0" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.9.tgz", + "integrity": "sha512-vsl5/ueE3Jf0f6XzB0ECHHMsd5A0Yu6StElb8a+XsubZW7kHNAOw4Y3TSSuDzKEpLnJ92nbMy1Zl+KLGCE6NaA==", + "dev": true, + "dependencies": { + "@types/mocha": "^10.0.2", + "c8": "^9.1.0", + "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", + "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "jszip": "^3.10.1", + "ora": "^7.0.1", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/test-electron/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/c8": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", + "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", + "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "8.1.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "peer": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/msbuild-editor-vscode/package.json b/msbuild-editor-vscode/package.json new file mode 100644 index 00000000..b0b5d96f --- /dev/null +++ b/msbuild-editor-vscode/package.json @@ -0,0 +1,180 @@ +{ + "displayName": "MSBuild Editor", + "description": "Editor for MSBuild files that supports IntelliSense, quick info, navigation, analyzers and refactorings.", + "name": "msbuild-editor", + "publisher": "mhutch", + "version": "0.0.1", + "license": "MIT", + "author": { + "name": "Mikayla Hutchinson" + }, + "repository": { + "type": "git", + "url": "https://github.com/mhutch/MonoDevelop.MSBuildEditor.git" + }, + "bugs": { + "url": "https://github.com/mhutch/MonoDevelop.MSBuildEditor" + }, + "qna": "https://github.com/mhutch/MonoDevelop.MSBuildEditor/discussions", + "engines": { + "vscode": "^1.89.0" + }, + "categories": [ + "Programming Languages" + ], + "activationEvents": [], + "main": "./dist/extension.js", + "scripts": { + "vscode:prepublish": "npm run package", + "compile": "npm run check-types && npm run lint && node esbuild.js", + "watch": "npm-run-all -p watch:*", + "watch:esbuild": "node esbuild.js --watch", + "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", + "package": "npm run check-types && npm run lint && node esbuild.js --production", + "compile-tests": "tsc -p . --outDir out", + "watch-tests": "tsc -p . -w --outDir out", + "pretest": "npm run compile-tests && npm run compile && npm run lint", + "check-types": "tsc --noEmit", + "lint": "eslint src --ext ts", + "test": "vscode-test" + }, + "dependencies": { + "@vscode/extension-telemetry": "^0.9.0", + "rxjs": "6.6.7", + "semver": "7.5.4", + "uuid": "^9.0.0", + "vscode-languageclient": "^9.0.1" + }, + "extensionDependencies": [ + "ms-dotnettools.vscode-dotnet-runtime" + ], + "devDependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^18.19.33", + "@types/semver": "7.3.13", + "@types/uuid": "^9.0.1", + "@types/vscode": "^1.89.0", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "@vscode/test-cli": "^0.0.9", + "@vscode/test-electron": "^2.3.9", + "esbuild": "^0.20.2", + "eslint": "^8.57.0", + "npm-run-all": "^4.1.5", + "typescript": "^5.4.5" + }, + "contributes": { + "languages": [ + { + "id": "msbuild", + "aliases": [ + "MSBuild", + "msbuild" + ], + "extensions": [ + ".targets", + ".props", + ".tasks", + ".overridetasks", + ".csproj", + ".vbproj", + ".fsproj", + ".xproj", + ".vcxproj", + ".sfxproj", + ".esproj", + ".proj", + ".user", + ".pubxml" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "msbuild", + "scopeName": "text.msbuild", + "path": "syntaxes/msbuild.tmLanguage.json" + } + ], + "configuration": [ + { + "title": "Language Server", + "order": 1, + "properties": { + "msbuild.server.dotnetPath": { + "type": "string", + "scope": "machine-overridable", + "description": "%configuration.msbuild.server.dotnetPath%" + }, + "msbuild.server.path": { + "type": "string", + "scope": "machine-overridable", + "description": "%configuration.msbuild.server.path%" + }, + "msbuild.server.startTimeout": { + "type": "number", + "scope": "machine-overridable", + "default": 30000, + "description": "%configuration.msbuild.server.startTimeout%" + }, + "msbuild.server.waitForDebugger": { + "type": "boolean", + "scope": "machine-overridable", + "default": false, + "description": "%configuration.msbuild.server.waitForDebugger%" + }, + "msbuild.server.trace": { + "scope": "window", + "type": "string", + "enum": [ + "Trace", + "Debug", + "Information", + "Warning", + "Error", + "Critical", + "None" + ], + "default": "Information", + "description": "%configuration.msbuild.server.trace%" + }, + "msbuild.server.crashDumpPath": { + "scope": "machine-overridable", + "type": "string", + "default": null, + "description": "%configuration.msbuild.server.crashDumpPath%" + }, + "msbuild.server.suppressLspErrorToasts": { + "type": "boolean", + "default": false, + "description": "%configuration.msbuild.server.suppressLspErrorToasts%" + } + } + } + ], + "jsonValidation": [ + { + "fileMatch": "*.buildSchema.json", + "url": "https://raw.githubusercontent.com/mhutch/MonoDevelop.MSBuildEditor/main/MonoDevelop.MSBuild/Schemas/buildschema.json" + } + ], + "commands": [ + { + "command": "msbuild.reportIssue", + "title": "%command.msbuild.reportIssue%", + "category": "MSBuild" + }, + { + "command": "msbuild.restartServer", + "title": "%command.msbuild.restartServer%", + "category": "MSBuild" + }, + { + "command": "msbuild.showOutputWindow", + "title": "%command.msbuild.showOutputWindow%", + "category": "MSBuild" + } + ] + } +} diff --git a/msbuild-editor-vscode/package.nls.json b/msbuild-editor-vscode/package.nls.json new file mode 100644 index 00000000..e46f2c44 --- /dev/null +++ b/msbuild-editor-vscode/package.nls.json @@ -0,0 +1,12 @@ +{ + "command.msbuild.reportIssue": "Report an MSBuild Editor issue", + "command.msbuild.restartServer": "Restart MSBuild Language Server", + "command.msbuild.showOutputWindow": "Show MSBuild output window", + "configuration.msbuild.server.dotnetPath": "Specifies the path to a dotnet installation directory to use instead of the default system one. This only influences the dotnet installation to use for hosting the language server itself. Example: \"/home/username/my-custom-dotnet-directory\".", + "configuration.msbuild.server.path": "Specifies the absolute path to the server executable. When left empty the version pinned to the MSBuild Editor Extension is used.", + "configuration.msbuild.server.startTimeout": "Specifies a timeout (in ms) for the client to successfully start and connect to the language server.", + "configuration.msbuild.server.waitForDebugger": "Passes the --debug flag when launching the server to allow a debugger to be attached.", + "configuration.msbuild.server.trace": "Sets the logging level for the language server", + "configuration.msbuild.server.crashDumpPath": "Sets a folder path where crash dumps are written to if the language server crashes. Must be writeable by the user.", + "configuration.msbuild.server.suppressLspErrorToasts": "Suppresses error toasts from showing up if the server encounters a recoverable error." +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/completionItemMiddleware.ts b/msbuild-editor-vscode/src/completionItemMiddleware.ts new file mode 100644 index 00000000..e4926094 --- /dev/null +++ b/msbuild-editor-vscode/src/completionItemMiddleware.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient'; + +export default async function completionItemMiddleware( + item: vscode.CompletionItem, + token: vscode.CancellationToken, + next: lsp.ResolveCompletionItemSignature, +): Promise { + const result = await next(item, token); + if (!result) { + return result; + } + if (result.documentation instanceof vscode.MarkdownString) { + result.documentation.supportThemeIcons = true; + result.documentation.isTrusted = { + enabledCommands: [ + 'editor.action.goToReferences' + ] + }; + } + return result; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/extension.ts b/msbuild-editor-vscode/src/extension.ts new file mode 100644 index 00000000..67ead595 --- /dev/null +++ b/msbuild-editor-vscode/src/extension.ts @@ -0,0 +1,134 @@ +import * as vscode from 'vscode'; +import * as roslyn from './roslynImport/main'; +import { workspace, ExtensionContext } from 'vscode'; +import { msbuildEditorOptions } from './options'; +import hoverMiddleware from './hoverMiddleware'; +import completionItemMiddleware from './completionItemMiddleware'; +import { RoslynLanguageServerDefinition } from './roslynImport/lsptoolshost/roslynLanguageServer'; +import path from 'path'; + +const msbuildDocumentSelector: vscode.DocumentSelector = [{ scheme: 'file', language: 'msbuild' }]; + +export function activate(context: ExtensionContext) { + const lspDefinition : RoslynLanguageServerDefinition = { + clientId: 'msbuild-lsp', + clientName: 'MSBuild LSP', + clientOptions: { + // FIXME: use msbuildDocumentSelector here, currently results in weird type error + documentSelector: [{ scheme: 'file', language: 'msbuild' }], + synchronize: { + fileEvents: [], + }, + markdown: { + supportHtml: true + }, + middleware: { + provideHover: hoverMiddleware, + resolveCompletionItem: completionItemMiddleware + } + }, + serverPathEnvVar: 'MSBUILD_LANGUAGE_SERVER_PATH', + bundledServerPath: path.join(context.extensionPath, 'server', 'MSBuildLanguageServer.dll'), + commandIdPrefix: 'msbuild' + }; + + roslyn.activate(context, msbuildEditorOptions, lspDefinition); + + context.subscriptions.push( + handleCompletionTrigger() + ); + + /* + let disposable = vscode.commands.registerCommand('msbuild-editor.helloWorld', () => { + vscode.window.showInformationMessage('Hello World from msbuild-editor!'); + }); + context.subscriptions.push(disposable); + */ +} + +export function deactivate() { +} + +// trigger completion automatically in a few places where VS Code would not normally do so +function handleCompletionTrigger(): vscode.Disposable { + return vscode.workspace.onDidChangeTextDocument(async (e) => { + if (!vscode.languages.match(msbuildDocumentSelector, e.document)) { + return; + } + + // don't support completion for multi-caret editing + if (e.contentChanges.length !== 1) { + return; + } + + // only if the change is in the active document + if(e.document !== vscode.window.activeTextEditor?.document) { + return; + } + + const change = e.contentChanges[0]; + + if(isExpressionCompletionInsertion(change) || isAttributeStartCharTrigger(change, e.document)) { + await vscode.commands.executeCommand('editor.action.triggerSuggest'); + } + }); +} + +// Trigger completion immediately after using completion to commit $(), @(), or %(). +// If the user manually types the expression out, then completion will be triggered +// by the server when they type '(', as it is a trigger char. +function isExpressionCompletionInsertion(change: vscode.TextDocumentContentChangeEvent) : boolean +{ + switch(change.text) { + case '$()': + case '@()': + case '%()': + case '$(': + case '@(': + case '%(': + // we cannot determine whether the change came from completion or from paste etc, so this could + // be accidentally triggered. however, we can at least verify that the caret is where we expect. + return vscode.window.activeTextEditor?.selection.start.compareTo(change.range.start) === 0; + default: + return false; + } +} + +// For some reason VS Code does not trigger completion when typing the first char in an empty attribute. +// This function attempts to trigger completion in that case i.e. typing an alphanumeric character between quotes. +// It's pretty naive and we could probably check more context to make sure we're inside an attribute. +function isAttributeStartCharTrigger(change: vscode.TextDocumentContentChangeEvent, document: vscode.TextDocument) : boolean +{ + // trigger for any alphanumeric char or underscore + if(!change.text.match(/[a-zA-Z0-9_]/)) { + return false; + } + + // we're checking if we're inside quotes, so can't be at the start of the line + if (change.range.start.character === 0) { + return false; + } + + // get the chars before and after the change. + var range = new vscode.Range(change.range.start.translate(0, -1), change.range.end.translate(0, 1)); + var text = document.getText(range); + + // the previous char must be a an attribute quote + var quoteChar = text[0]; + if(!isAttributeQuote(quoteChar)) { + return false; + } + + // to trigger completion after typing an alphanumeric char after a quote, the next char must be the matching quote or EOL + var nextCharIdx = change.range.start.character === 0 ? 1 : 2; + if(text.length > nextCharIdx && text[nextCharIdx] !== quoteChar){ + return false; + } + + return true; +} + +function isAttributeQuote(text : string) : boolean +{ + return text === '"' || text === "'"; +} diff --git a/msbuild-editor-vscode/src/hoverMiddleware.ts b/msbuild-editor-vscode/src/hoverMiddleware.ts new file mode 100644 index 00000000..beabd461 --- /dev/null +++ b/msbuild-editor-vscode/src/hoverMiddleware.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient'; + +export default async function hoverMiddleware( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + next: lsp.ProvideHoverSignature, +): Promise { + const result = await next(document, position, token); + if (!result) { + return result; + } + result.contents.map(content => { + if (content instanceof vscode.MarkdownString) { + content.supportThemeIcons = true; + content.isTrusted = { + enabledCommands: [ + 'editor.action.goToReferences' + ] + }; + } + }); + return result; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/options.ts b/msbuild-editor-vscode/src/options.ts new file mode 100644 index 00000000..e307db88 --- /dev/null +++ b/msbuild-editor-vscode/src/options.ts @@ -0,0 +1,39 @@ +import { RoslynLanguageServerOptions } from "./roslynImport/lsptoolshost/roslynLanguageServer"; +import { readOption } from "./roslynImport/shared/options"; + +export interface MSBuildEditorOptions extends RoslynLanguageServerOptions +{ +} + +export class MSBuildEditorOptionsImpl implements MSBuildEditorOptions +{ + public get serverPath() { + return readOption('msbuild.server.path', ''); + } + + public get startTimeout(){ + return readOption('msbuild.server.startTimeout', 30000); + } + + public get waitForDebugger(){ + return readOption('msbuild.server.waitForDebugger', false); + } + + public get logLevel(){ + return readOption('msbuild.server.logLevel', 'Information'); + } + + public get suppressLspErrorToasts(){ + return readOption('msbuild.server.suppressLspErrorToasts', false); + } + + public get dotnetPath(){ + return readOption('msbuild.server.dotnetPath', ''); + } + + public get crashDumpPath(){ + return readOption('msbuild.server.crashDumpPath', undefined); + } +} + +export const msbuildEditorOptions : MSBuildEditorOptions = new MSBuildEditorOptionsImpl(); \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/common.ts b/msbuild-editor-vscode/src/roslynImport/common.ts new file mode 100644 index 00000000..fc85431d --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/common.ts @@ -0,0 +1,236 @@ +//https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/common.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { AbsolutePath } from './packageManager/absolutePath'; + +let extensionPath: string; + +export function setExtensionPath(path: string) { + extensionPath = path; +} + +export function getExtensionPath() { + if (!extensionPath) { + throw new Error('Failed to set extension path'); + } + + return extensionPath; +} + +export function getUnixTempDirectory() { + const envTmp = process.env.TMPDIR; + if (!envTmp) { + return '/tmp/'; + } + + return envTmp; +} + +export function sum(arr: T[], selector: (item: T) => number): number { + return arr.reduce((prev, curr) => prev + selector(curr), 0); +} + +export async function mapAsync( + array: T1[], + selector: (value: T1, index: number, array: T1[]) => Promise +): Promise { + return Promise.all(array.map(selector)); +} + +export async function filterAsync( + array: T[], + predicate: (value: T, index: number, array: T[]) => Promise +): Promise { + const filterMap = await mapAsync(array, predicate); + return array.filter((_, index) => filterMap[index]); +} + +/** Retrieve the length of an array. Returns 0 if the array is `undefined`. */ +export function safeLength(arr: T[] | undefined) { + return arr ? arr.length : 0; +} + +export async function execChildProcess( + command: string, + workingDirectory: string = getExtensionPath(), + env: NodeJS.ProcessEnv = {} +): Promise { + return new Promise((resolve, reject) => { + cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024, env: env }, (error, stdout, stderr) => { + if (error) { + reject( + new Error(`${error} +${stdout} +${stderr}`) + ); + } else if (stderr && !stderr.includes('screen size is bogus')) { + reject(new Error(stderr)); + } else { + resolve(stdout); + } + }); + }); +} + +export async function getUnixChildProcessIds(pid: number): Promise { + return new Promise((resolve, reject) => { + cp.exec('ps -A -o ppid,pid', (error, stdout, stderr) => { + if (error) { + return reject(error); + } + + if (stderr && !stderr.includes('screen size is bogus')) { + return reject(new Error(stderr)); + } + + if (!stdout) { + return resolve([]); + } + + const lines = stdout.split(os.EOL); + const pairs = lines.map((line) => line.trim().split(/\s+/)); + + const children = []; + + for (const pair of pairs) { + const ppid = parseInt(pair[0]); + if (ppid === pid) { + children.push(parseInt(pair[1])); + } + } + + resolve(children); + }); + }); +} + +export async function fileExists(filePath: string): Promise { + return new Promise((resolve) => { + fs.stat(filePath, (err, stats) => { + if (stats && stats.isFile()) { + resolve(true); + } else { + resolve(false); + } + }); + }); +} + +export async function deleteIfExists(filePath: string): Promise { + return fileExists(filePath).then(async (exists: boolean) => { + return new Promise((resolve, reject) => { + if (!exists) { + return resolve(); + } + + fs.unlink(filePath, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); + }); +} + +export enum InstallFileType { + Begin, + Lock, +} + +export function getInstallFilePath(folderPath: AbsolutePath, type: InstallFileType): string { + const installFile = 'install.' + InstallFileType[type]; + return path.resolve(folderPath.value, installFile); +} + +export async function installFileExists(folderPath: AbsolutePath, type: InstallFileType): Promise { + return fileExists(getInstallFilePath(folderPath, type)); +} + +export async function touchInstallFile(folderPath: AbsolutePath, type: InstallFileType): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(getInstallFilePath(folderPath, type), '', (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); +} + +export async function deleteInstallFile(folderPath: AbsolutePath, type: InstallFileType): Promise { + return new Promise((resolve, reject) => { + fs.unlink(getInstallFilePath(folderPath, type), (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); +} + +export function convertNativePathToPosix(pathString: string): string { + const parts = pathString.split(path.sep); + return parts.join(path.posix.sep); +} + +/** + * This function checks to see if a subfolder is part of a parent folder. + * + * Assumes subfolder and parent folder are absolute paths and have consistent casing. + * + * @param subfolder subfolder to check if it is part of the parent folder parameter + * @param parentFolder folder to check aganist + */ +export function isSubfolderOf(subfolder: string, parentFolder: string): boolean { + const subfolderArray: string[] = subfolder.split(path.sep); + const parentFolderArray: string[] = parentFolder.split(path.sep); + + // Check to see that every sub directory in subfolder exists in folder. + return ( + parentFolderArray.length <= subfolder.length && + parentFolderArray.every((subpath, index) => subfolderArray[index] === subpath) + ); +} + +/** + * Find PowerShell executable from PATH (for Windows only). + */ +export function findPowerShell(): string | undefined { + const dirs: string[] = (process.env.PATH || '') + .replace(/"+/g, '') + .split(';') + .filter((x) => x); + const names: string[] = ['pwsh.exe', 'powershell.exe']; + for (const name of names) { + const candidates: string[] = dirs.reduce((paths, dir) => [...paths, path.join(dir, name)], []); + for (const candidate of candidates) { + try { + if (fs.statSync(candidate).isFile()) { + return name; + } + } catch (e) { + /* empty */ + } + } + } +} + +export function isNotNull(value: T): asserts value is NonNullable { + if (value === null || value === undefined) { + throw new Error('value is null or undefined.'); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/compositeDisposable.ts b/msbuild-editor-vscode/src/roslynImport/compositeDisposable.ts new file mode 100644 index 00000000..d6d74c27 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/compositeDisposable.ts @@ -0,0 +1,33 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/compositeDisposable.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Subscription } from 'rxjs'; +import Disposable, { IDisposable } from './disposable'; + +export default class CompositeDisposable extends Disposable { + private disposables = new Subscription(); + + constructor(...disposables: IDisposable[]) { + super(() => this.disposables.unsubscribe()); + + for (const disposable of disposables) { + if (disposable) { + this.add(disposable); + } else { + throw new Error('null disposables are not supported'); + } + } + } + + public add(disposable: IDisposable) { + if (!disposable) { + throw new Error('disposable cannot be null'); + } + + this.disposables.add(() => disposable.dispose()); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/coreclrDebug/util.ts b/msbuild-editor-vscode/src/roslynImport/coreclrDebug/util.ts new file mode 100644 index 00000000..53acf2f4 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/coreclrDebug/util.ts @@ -0,0 +1,164 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/coreclrDebug/util.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as semver from 'semver'; +import * as os from 'os'; +import { PlatformInformation } from '../shared/platform'; +import { getDotnetInfo } from '../shared/utils/getDotnetInfo'; +import { DotnetInfo } from '../shared/utils/dotnetInfo'; + +const MINIMUM_SUPPORTED_DOTNET_CLI = '1.0.0'; + +export class CoreClrDebugUtil { + private _extensionDir = ''; + private _debugAdapterDir = ''; + private _installCompleteFilePath = ''; + + constructor(extensionDir: string) { + this._extensionDir = extensionDir; + this._debugAdapterDir = path.join(this._extensionDir, '.debugger'); + this._installCompleteFilePath = path.join(this._debugAdapterDir, 'install.complete'); + } + + public extensionDir(): string { + if (this._extensionDir === '') { + throw new Error(vscode.l10n.t('Failed to set extension directory')); + } + return this._extensionDir; + } + + public debugAdapterDir(): string { + if (this._debugAdapterDir === '') { + throw new Error(vscode.l10n.t('Failed to set debugadpter directory')); + } + return this._debugAdapterDir; + } + + public installCompleteFilePath(): string { + if (this._installCompleteFilePath === '') { + throw new Error(vscode.l10n.t('Failed to set install complete file path')); + } + return this._installCompleteFilePath; + } + + public static async writeEmptyFile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(path, '', (err) => { + if (err) { + reject(err.code); + } else { + resolve(); + } + }); + }); + } + + // This function checks for the presence of dotnet on the path and ensures the Version + // is new enough for us. + public async checkDotNetCli(dotNetCliPaths: string[]): Promise { + try { + const dotnetInfo = await getDotnetInfo(dotNetCliPaths); + if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) { + throw new Error( + vscode.l10n.t( + `The .NET Core SDK located on the path is too old. .NET Core debugging will not be enabled. The minimum supported version is {0}.`, + MINIMUM_SUPPORTED_DOTNET_CLI + ) + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : `${error}`; + throw new Error( + vscode.l10n.t( + `The .NET Core SDK cannot be located: {0}. .NET Core debugging will not be enabled. Make sure the .NET Core SDK is installed and is on the path.`, + message + ) + ); + } + } + + public static isMacOSSupported(): boolean { + // .NET Core 2.0 requires macOS 10.12 (Sierra), which is Darwin 16.0+ + // Darwin version chart: https://en.wikipedia.org/wiki/Darwin_(operating_system) + return semver.gte(os.release(), '16.0.0'); + } + + public static existsSync(path: string): boolean { + try { + fs.accessSync(path, fs.constants.F_OK); + return true; + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { + return false; + } else { + throw Error(error.code); + } + } + } + + public static getPlatformExeExtension(): string { + if (process.platform === 'win32') { + return '.exe'; + } + + return ''; + } +} + +const MINIMUM_SUPPORTED_ARM64_DOTNET_CLI = '6.0.0'; + +export function getTargetArchitecture( + platformInfo: PlatformInformation, + launchJsonTargetArchitecture: string | undefined, + dotnetInfo: DotnetInfo +): string { + if (!platformInfo.isMacOS() && !platformInfo.isWindows()) { + // Nothing to do here. + return ''; + } + + // On Windows ARM64 and Apple M1 Machines, we need to determine if we need to use the 'x86_64' or 'arm64' debugger. + + // 'targetArchitecture' is specified in launch.json configuration, use that. + if (launchJsonTargetArchitecture) { + if (launchJsonTargetArchitecture !== 'x86_64' && launchJsonTargetArchitecture !== 'arm64') { + throw new Error( + vscode.l10n.t( + `The value '{0}' for 'targetArchitecture' in launch configuraiton is invalid. Expected 'x86_64' or 'arm64'.`, + launchJsonTargetArchitecture + ) + ); + } + return launchJsonTargetArchitecture; + } + + // If we are lower than .NET 6, use 'x86_64' since 'arm64' was not supported until .NET 6. + if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_ARM64_DOTNET_CLI)) { + return 'x86_64'; + } + + // Otherwise, look at the runtime ID. + if (!dotnetInfo.RuntimeId) { + throw new Error( + vscode.l10n.t( + `Unable to determine RuntimeId. Please set 'targetArchitecture' in your launch.json configuration.` + ) + ); + } + + if (dotnetInfo.RuntimeId.includes('arm64')) { + return 'arm64'; + } else if (dotnetInfo.RuntimeId.includes('x64')) { + return 'x86_64'; + } + + throw new Error(vscode.l10n.t("Unexpected RuntimeId '{0}'.", dotnetInfo.RuntimeId)); +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/disposable.ts b/msbuild-editor-vscode/src/roslynImport/disposable.ts new file mode 100644 index 00000000..d41a5e61 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/disposable.ts @@ -0,0 +1,32 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/disposable.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Subscription } from 'rxjs'; + +export default class Disposable implements IDisposable { + private onDispose: { (): void }; + + constructor(onDispose: { (): void } | Subscription) { + if (!onDispose) { + throw new Error('onDispose cannot be null or empty.'); + } + + if (onDispose instanceof Subscription) { + this.onDispose = () => onDispose.unsubscribe(); + } else { + this.onDispose = onDispose; + } + } + + public dispose = (): void => { + this.onDispose(); + }; +} + +export interface IDisposable { + dispose: () => void; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/eventStream.ts b/msbuild-editor-vscode/src/roslynImport/eventStream.ts new file mode 100644 index 00000000..fecccb84 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/eventStream.ts @@ -0,0 +1,24 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/eventStream.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Subject, Subscription } from 'rxjs'; +import { BaseEvent } from './omnisharp/loggingEvents'; + +export class EventStream { + private sink: Subject; + + constructor() { + this.sink = new Subject(); + } + + public post(event: BaseEvent) { + this.sink.next(event); + } + + public subscribe(eventHandler: (event: BaseEvent) => void): Subscription { + return this.sink.subscribe(eventHandler); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/commands.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/commands.ts new file mode 100644 index 00000000..35d8ff8c --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/commands.ts @@ -0,0 +1,42 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/commands.ts +// reduced down to just a few reusable commands and parameterized extension-specific info + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { RoslynLanguageServer } from './roslynLanguageServer'; +import reportIssue from '../shared/reportIssue'; +import { getDotnetInfo } from '../shared/utils/getDotnetInfo'; +import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver'; + +export function registerCommands( + commandPrefix:string, + context: vscode.ExtensionContext, + languageServer: RoslynLanguageServer, + hostExecutableResolver: IHostExecutableResolver, + outputChannel: vscode.OutputChannel +) { + context.subscriptions.push( + vscode.commands.registerCommand(`${commandPrefix}.restartServer`, async () => restartServer(languageServer)) + ); + context.subscriptions.push( + vscode.commands.registerCommand(`${commandPrefix}.reportIssue`, async () => + reportIssue( + context, + `${commandPrefix}.server.trace`, + getDotnetInfo, + hostExecutableResolver + ) + ) + ); + context.subscriptions.push( + vscode.commands.registerCommand(`${commandPrefix}.showOutputWindow`, async () => outputChannel.show()) + ); +} + +async function restartServer(languageServer: RoslynLanguageServer): Promise { + await languageServer.restart(); +} diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/dotnetRuntimeExtensionResolver.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/dotnetRuntimeExtensionResolver.ts new file mode 100644 index 00000000..ffd49efa --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/dotnetRuntimeExtensionResolver.ts @@ -0,0 +1,273 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/dotnetRuntimeExtensionResolver.ts +// parameterized options + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as semver from 'semver'; +import { HostExecutableInformation } from '../shared/constants/hostExecutableInformation'; +import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver'; +import { PlatformInformation } from '../shared/platform'; +import { existsSync } from 'fs'; +import { getDotnetInfo } from '../shared/utils/getDotnetInfo'; +import { readFile } from 'fs/promises'; +import { RuntimeInfo } from '../shared/utils/dotnetInfo'; + +export const DotNetRuntimeVersion = '8.0'; + +export interface DotnetRuntimeResolverOptions { + readonly dotnetPath: string; + readonly crashDumpPath: string | undefined; +} + +interface IDotnetAcquireResult { + dotnetPath: string; +} + +/** + * Resolves the dotnet runtime for a server executable from given options and the dotnet runtime VSCode extension. + */ +export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver { + constructor( + private platformInfo: PlatformInformation, + /** + * This is a function instead of a string because the server path can change while the extension is active (when the option changes). + */ + private getServerPath: (platform: PlatformInformation) => string, + private channel: vscode.OutputChannel, + private extensionPath: string, + private extensionId: string, + private options: DotnetRuntimeResolverOptions + ) {} + + private hostInfo: HostExecutableInformation | undefined; + + async getHostExecutableInfo(): Promise { + let dotnetRuntimePath = this.options.dotnetPath; + const serverPath = this.getServerPath(this.platformInfo); + + // Check if we can find a valid dotnet from dotnet --version on the PATH. + if (!dotnetRuntimePath) { + const dotnetPath = await this.findDotnetFromPath(); + if (dotnetPath) { + return { + version: '' /* We don't need to know the version - we've already verified its high enough */, + path: dotnetPath, + env: this.getEnvironmentVariables(dotnetPath), + }; + } + } + + // We didn't find it on the path, see if we can install the correct runtime using the runtime extension. + if (!dotnetRuntimePath) { + const dotnetInfo = await this.acquireDotNetProcessDependencies(serverPath); + dotnetRuntimePath = path.dirname(dotnetInfo.path); + } + + const dotnetExecutableName = this.getDotnetExecutableName(); + const dotnetExecutablePath = path.join(dotnetRuntimePath, dotnetExecutableName); + if (!existsSync(dotnetExecutablePath)) { + throw new Error(`Cannot find dotnet path '${dotnetExecutablePath}'`); + } + + return { + version: '' /* We don't need to know the version - we've already downloaded the correct one */, + path: dotnetExecutablePath, + env: this.getEnvironmentVariables(dotnetExecutablePath), + }; + } + + private getEnvironmentVariables(dotnetExecutablePath: string): NodeJS.ProcessEnv { + // Take care to always run .NET processes on the runtime that we intend. + // The dotnet.exe we point to should not go looking for other runtimes. + const env: NodeJS.ProcessEnv = { ...process.env }; + env.DOTNET_ROOT = path.dirname(dotnetExecutablePath); + env.DOTNET_MULTILEVEL_LOOKUP = '0'; + // Save user's DOTNET_ROOT env-var value so server can recover the user setting when needed + env.DOTNET_ROOT_USER = process.env.DOTNET_ROOT ?? 'EMPTY'; + + if (this.options.crashDumpPath) { + // Enable dump collection + env.DOTNET_DbgEnableMiniDump = '1'; + // Collect heap dump + env.DOTNET_DbgMiniDumpType = '2'; + // Collect crashreport.json with additional thread and stack frame information. + env.DOTNET_EnableCrashReport = '1'; + // The dump file name format is ..dmp + env.DOTNET_DbgMiniDumpName = path.join(this.options.crashDumpPath, '%e.%p.dmp'); + } + + return env; + } + + /** + * Acquires the .NET runtime if it is not already present. + * @returns The path to the .NET runtime + */ + private async acquireRuntime(): Promise { + if (this.hostInfo) { + return this.hostInfo; + } + + let status = await vscode.commands.executeCommand('dotnet.acquireStatus', { + version: DotNetRuntimeVersion, + requestingExtensionId: this.extensionId, + }); + if (status === undefined) { + await vscode.commands.executeCommand('dotnet.showAcquisitionLog'); + + status = await vscode.commands.executeCommand('dotnet.acquire', { + version: DotNetRuntimeVersion, + requestingExtensionId: this.extensionId, + }); + if (!status?.dotnetPath) { + throw new Error('Could not resolve the dotnet path!'); + } + } + + return (this.hostInfo = { + version: DotNetRuntimeVersion, + path: status.dotnetPath, + env: process.env, + }); + } + + /** + * Acquires the .NET runtime and any other dependencies required to spawn a particular .NET executable. + * @param path The path to the entrypoint assembly. Typically a .dll. + */ + private async acquireDotNetProcessDependencies(path: string): Promise { + const dotnetInfo = await this.acquireRuntime(); + + const args = [path]; + // This will install any missing Linux dependencies. + await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', { + command: dotnetInfo.path, + arguments: args, + }); + + return dotnetInfo; + } + + /** + * Checks dotnet --version to see if the value on the path is greater than the minimum required version. + * This is adapted from similar O# server logic and should be removed when we have a stable acquisition extension. + * @returns true if the dotnet version is greater than the minimum required version, false otherwise. + */ + private async findDotnetFromPath(): Promise { + try { + const dotnetInfo = await getDotnetInfo([]); + + const extensionArchitecture = await this.getArchitectureFromTargetPlatform(); + const dotnetArchitecture = dotnetInfo.Architecture; + + // If the extension architecture is defined, we check that it matches the dotnet architecture. + // If its undefined we likely have a platform neutral server and assume it can run on any architecture. + if (extensionArchitecture && extensionArchitecture !== dotnetArchitecture) { + throw new Error( + `The architecture of the .NET runtime (${dotnetArchitecture}) does not match the architecture of the extension (${extensionArchitecture}).` + ); + } + + // Verify that the dotnet we found includes a runtime version that is compatible with our requirement. + const requiredRuntimeVersion = semver.parse(`${DotNetRuntimeVersion}.0`); + if (!requiredRuntimeVersion) { + throw new Error(`Unable to parse minimum required version ${DotNetRuntimeVersion}`); + } + + const coreRuntimeVersions = dotnetInfo.Runtimes['Microsoft.NETCore.App']; + let matchingRuntime: RuntimeInfo | undefined = undefined; + for (const runtime of coreRuntimeVersions) { + // We consider a match if the runtime is greater than or equal to the required version since we roll forward. + if (semver.gte(runtime.Version, requiredRuntimeVersion)) { + matchingRuntime = runtime; + break; + } + } + + if (!matchingRuntime) { + throw new Error( + `No compatible .NET runtime found. Minimum required version is ${DotNetRuntimeVersion}.` + ); + } + + // The .NET install layout is a well known structure on all platforms. + // See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout + // + // Therefore we know that the runtime path is always in /shared/ + // and the dotnet executable is always at /dotnet(.exe). + // + // Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!) + // we know the dotnet executable will be two folders up in the install root. + const runtimeFolderPath = matchingRuntime.Path; + const installFolder = path.dirname(path.dirname(runtimeFolderPath)); + const dotnetExecutablePath = path.join(installFolder, this.getDotnetExecutableName()); + if (!existsSync(dotnetExecutablePath)) { + throw new Error( + `dotnet executable path does not exist: ${dotnetExecutablePath}, dotnet installation may be corrupt.` + ); + } + + this.channel.appendLine(`Using dotnet configured on PATH`); + return dotnetExecutablePath; + } catch (e) { + this.channel.appendLine( + 'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime' + ); + if (e instanceof Error) { + this.channel.appendLine(e.message); + } + } + + return undefined; + } + + private async getArchitectureFromTargetPlatform(): Promise { + const vsixManifestFile = path.join(this.extensionPath, '.vsixmanifest'); + if (!existsSync(vsixManifestFile)) { + // This is not an error as normal development F5 builds do not generate a .vsixmanifest file. + this.channel.appendLine( + `Unable to find extension target platform - no vsix manifest file exists at ${vsixManifestFile}` + ); + return undefined; + } + + const contents = await readFile(vsixManifestFile, 'utf-8'); + const targetPlatformMatch = /TargetPlatform="(.*)"/.exec(contents); + if (!targetPlatformMatch) { + throw new Error(`Could not find extension target platform in ${vsixManifestFile}`); + } + + const targetPlatform = targetPlatformMatch[1]; + + // The currently known extension platforms are taken from here: + // https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions + switch (targetPlatform) { + case 'win32-x64': + case 'linux-x64': + case 'alpine-x64': + case 'darwin-x64': + return 'x64'; + case 'win32-ia32': + return 'x86'; + case 'win32-arm64': + case 'linux-arm64': + case 'alpine-arm64': + case 'darwin-arm64': + return 'arm64'; + case 'linux-armhf': + case 'web': + return undefined; + default: + throw new Error(`Unknown extension target platform: ${targetPlatform}`); + } + } + + private getDotnetExecutableName(): string { + return this.platformInfo.isWindows() ? 'dotnet.exe' : 'dotnet'; + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/optionChanges.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/optionChanges.ts new file mode 100644 index 00000000..9c37559b --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/optionChanges.ts @@ -0,0 +1,31 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/optionChanges.ts +// altered to deal with a single option type and to restart the server in a simpler way + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Observable } from 'rxjs'; +import { HandleOptionChanges, OptionChangeObserver } from '../shared/observers/optionChangeObserver'; +import Disposable from '../disposable'; +import { RoslynLanguageServer, RoslynLanguageServerOptions } from './roslynLanguageServer'; + +export function registerLanguageServerOptionChanges( + optionObservable: Observable, + languageServer : RoslynLanguageServer, + lspOptions : RoslynLanguageServerOptions, + lspOptionsThatTriggerReload : ReadonlyArray +): Disposable { + const optionChangeObserver: OptionChangeObserver = { + getRelevantOptions: () => lspOptionsThatTriggerReload, + handleOptionChanges(optionChanges) { + if (optionChanges.length !== 0) { + languageServer.restart(); + } + }, + }; + + const disposable = HandleOptionChanges(optionObservable, optionChangeObserver, lspOptions); + return disposable; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageClient.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageClient.ts new file mode 100644 index 00000000..d552e758 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageClient.ts @@ -0,0 +1,74 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/roslynLanguageClient.ts +// parameterized options + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + CancellationToken, + LanguageClient, + LanguageClientOptions, + MessageSignature, + ServerOptions, +} from 'vscode-languageclient/node'; +import CompositeDisposable from '../compositeDisposable'; +import { IDisposable } from '../disposable'; +import { RoslynLanguageServerOptions } from './roslynLanguageServer'; + +/** + * Implementation of the base LanguageClient type that allows for additional items to be disposed of + * when the base LanguageClient instance is disposed. + */ +export class RoslynLanguageClient extends LanguageClient { + private readonly _disposables: CompositeDisposable; + private readonly _lspOptions: RoslynLanguageServerOptions; + + constructor( + id: string, + name: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions, + lspOptions : RoslynLanguageServerOptions, + forceDebug?: boolean + ) { + super(id, name, serverOptions, clientOptions, forceDebug); + + this._disposables = new CompositeDisposable(); + this._lspOptions = lspOptions; + } + + override async dispose(timeout?: number | undefined): Promise { + this._disposables.dispose(); + return super.dispose(timeout); + } + + override handleFailedRequest( + type: MessageSignature, + token: CancellationToken | undefined, + error: any, + defaultValue: T, + showNotification?: boolean + ) { + // Temporarily allow LSP error toasts to be suppressed if configured. + // There are a few architectural issues preventing us from solving some of the underlying problems, + // for example Razor cohosting to fix text mismatch issues and unification of serialization libraries + // to fix URI identification issues. Once resolved, we should remove this option. + // + // See also https://github.com/microsoft/vscode-dotnettools/issues/722 + // https://github.com/dotnet/vscode-csharp/issues/6973 + // https://github.com/microsoft/vscode-languageserver-node/issues/1449 + if (this._lspOptions.suppressLspErrorToasts) { + return super.handleFailedRequest(type, token, error, defaultValue, false); + } + return super.handleFailedRequest(type, token, error, defaultValue, showNotification); + } + + /** + * Adds a disposable that should be disposed of when the LanguageClient instance gets disposed. + */ + public addDisposable(disposable: IDisposable) { + this._disposables.add(disposable); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageServer.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageServer.ts new file mode 100644 index 00000000..637a8407 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynLanguageServer.ts @@ -0,0 +1,544 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/roslynLanguageServer.ts +// this has been heavily edited to parameterize the extension-specific info +// and remove C#/Razor specific functionality + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as cp from 'child_process'; +import * as uuid from 'uuid'; +import * as net from 'net'; +import { registerCommands } from './commands'; +import { UriConverter } from './uriConverter'; + +import { + LanguageClientOptions, + ServerOptions, + State, + Trace, + RequestType, + RequestType0, + PartialResultParams, + ProtocolRequestType, + SocketMessageWriter, + SocketMessageReader, + MessageTransports, + RAL, + CancellationToken, + RequestHandler, + ResponseError, +} from 'vscode-languageclient/node'; +import { PlatformInformation } from '../shared/platform'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import { DotnetRuntimeExtensionResolver, DotnetRuntimeResolverOptions } from './dotnetRuntimeExtensionResolver'; +import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver'; +import { RoslynLanguageClient } from './roslynLanguageClient'; +import { registerLanguageServerOptionChanges } from './optionChanges'; +import { Observable } from 'rxjs'; +import { registerShowToastNotification } from './showToastNotification'; +import { NamedPipeInformation } from './roslynProtocol'; + +let _channel: vscode.OutputChannel; +let _traceChannel: vscode.OutputChannel; + +export interface RoslynLanguageServerDefinition { + clientId : string, + clientName : string, + clientOptions: LanguageClientOptions, + serverPathEnvVar: string, + bundledServerPath : string + commandIdPrefix : string, +} + +export interface RoslynLanguageServerOptions extends DotnetRuntimeResolverOptions { + readonly serverPath: string | undefined; + readonly startTimeout: number; + readonly waitForDebugger: boolean; + readonly logLevel: string; + readonly suppressLspErrorToasts: boolean; +} + +const RoslynLanguageOptionsThatTriggerReload: ReadonlyArray = [ + 'dotnetPath', + 'serverPath', + 'waitForDebugger', + 'logLevel', +]; + +export class RoslynLanguageServer { + /** + * The encoding to use when writing to and from the stream. + */ + private static readonly encoding: RAL.MessageBufferEncoding = 'utf-8'; + + /** + * The regular expression used to find the named pipe key in the LSP server's stdout stream. + */ + private static readonly namedPipeKeyRegex = /{"pipeName":"[^"]+"}/; + + /** + * The timeout for stopping the language server (in ms). + */ + private static _stopTimeout = 10000; + + constructor( + private _languageClient: RoslynLanguageClient, + private _platformInfo: PlatformInformation, + private _context: vscode.ExtensionContext, + private _telemetryReporter: TelemetryReporter, + private _options: RoslynLanguageServerOptions + ) { + this.registerSetTrace(); + + registerShowToastNotification(this._languageClient); + } + + private registerSetTrace() { + // Set the language client trace level based on the log level option. + // setTrace only works after the client is already running. + this._languageClient.onDidChangeState(async (state) => { + if (state.newState === State.Running) { + const languageClientTraceLevel = RoslynLanguageServer.GetTraceLevel(this._options.logLevel); + + await this._languageClient.setTrace(languageClientTraceLevel); + } + }); + } + + /** + * Resolves server options and starts the dotnet language server process. + * This promise will complete when the server starts. + */ + public static async initializeAsync( + lspDefinition : RoslynLanguageServerDefinition, + platformInfo: PlatformInformation, + hostExecutableResolver: IHostExecutableResolver, + context: vscode.ExtensionContext, + telemetryReporter: TelemetryReporter, + lspOptions: RoslynLanguageServerOptions + ): Promise { + const serverOptions: ServerOptions = async () => { + return await this.startServer( + platformInfo, + hostExecutableResolver, + context, + telemetryReporter, + lspDefinition.serverPathEnvVar, + lspDefinition.bundledServerPath, + lspOptions + ); + }; + + // TODO: clone instead of mutate, or replace lspDefinition.clientOptions with a set of specific things we can copy + lspDefinition.clientOptions.traceOutputChannel = _traceChannel; + lspDefinition.clientOptions.outputChannel = _channel; + lspDefinition.clientOptions.uriConverters = { + // VSCode encodes the ":" as "%3A" in file paths, for example "file:///c%3A/Users/dabarbet/source/repos/ConsoleApp8/ConsoleApp8/Program.cs". + // System.Uri does not decode the LocalPath property correctly into a valid windows path, instead you get something like + // "/c:/Users/dabarbet/source/repos/ConsoleApp8/ConsoleApp8/Program.cs" (note the incorrect forward slashes and prepended "/"). + // Properly decoded, it would look something like "c:\Users\dabarbet\source\repos\ConsoleApp8\ConsoleApp8\Program.cs" + // So instead we decode the URI here before sending to the server. + code2Protocol: UriConverter.serialize, + protocol2Code: UriConverter.deserialize, + }; + + /* + middleware: { + workspace: { + configuration: (params) => readConfigurations(params), + }, + }, + */ + + // Create the language client and start the client. + const client = new RoslynLanguageClient( + lspDefinition.clientId, + lspDefinition.clientName, + serverOptions, + lspDefinition.clientOptions, + lspOptions + ); + + client.registerProposedFeatures(); + + const server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, lspOptions); + + // Start the client. This will also launch the server process. + await client.start(); + return server; + } + + public async stop(): Promise { + await this._languageClient.stop(RoslynLanguageServer._stopTimeout); + } + + public async restart(): Promise { + await this._languageClient.restart(); + } + + /** + * Returns whether or not the underlying LSP server is running or not. + */ + public isRunning(): boolean { + return this._languageClient.state === State.Running; + } + + /** + * Makes an LSP request to the server with a given type and parameters. + */ + public async sendRequest( + type: RequestType, + params: Params, + token: vscode.CancellationToken + ): Promise { + if (!this.isRunning()) { + throw new Error('Tried to send request while server is not started.'); + } + + try { + const response = await this._languageClient.sendRequest(type, params, token); + return response; + } catch (e) { + throw this.convertServerError(type.method, e); + } + } + + /** + * Makes an LSP request to the server with a given type and no parameters + */ + public async sendRequest0( + type: RequestType0, + token: vscode.CancellationToken + ): Promise { + if (!this.isRunning()) { + throw new Error('Tried to send request while server is not started.'); + } + + try { + const response = await this._languageClient.sendRequest(type, token); + return response; + } catch (e) { + throw this.convertServerError(type.method, e); + } + } + + public async sendRequestWithProgress

( + type: ProtocolRequestType, + params: P, + onProgress: (p: PR) => Promise, + cancellationToken?: vscode.CancellationToken + ): Promise { + // Generate a UUID for our partial result token and apply it to our request. + const partialResultToken: string = uuid.v4(); + params.partialResultToken = partialResultToken; + // Register the callback for progress events. + const disposable = this._languageClient.onProgress(type, partialResultToken, async (partialResult) => { + await onProgress(partialResult); + }); + + try { + const response = await this._languageClient.sendRequest(type, params, cancellationToken); + return response; + } catch (e) { + throw this.convertServerError(type.method, e); + } finally { + disposable.dispose(); + } + } + + /** + * Sends an LSP notification to the server with a given method and parameters. + */ + public async sendNotification(method: string, params: Params): Promise { + if (!this.isRunning()) { + throw new Error('Tried to send request while server is not started.'); + } + + const response = await this._languageClient.sendNotification(method, params); + return response; + } + + public registerOnRequest( + type: RequestType, + handler: RequestHandler + ) { + this._languageClient.addDisposable(this._languageClient.onRequest(type, handler)); + } + + private convertServerError(request: string, e: any): Error { + let error: Error; + if (e instanceof ResponseError && e.code === -32800) { + // Convert the LSP RequestCancelled error (code -32800) to a CancellationError so we can handle cancellation uniformly. + error = new vscode.CancellationError(); + } else if (e instanceof Error) { + error = e; + } else if (typeof e === 'string') { + error = new Error(e); + } else { + error = new Error(`Unknown error: ${e.toString()}`); + } + + if (!(error instanceof vscode.CancellationError)) { + _channel.appendLine(`Error making ${request} request: ${error.message}`); + } + return error; + } + + private static async startServer( + platformInfo: PlatformInformation, + hostExecutableResolver: IHostExecutableResolver, + context: vscode.ExtensionContext, + telemetryReporter: TelemetryReporter, + serverPathEnvVar: string, + bundledServerPath : string, + options : RoslynLanguageServerOptions + ): Promise { + const serverPath = getServerPath(platformInfo, serverPathEnvVar, bundledServerPath, options); + + const dotnetInfo = await hostExecutableResolver.getHostExecutableInfo(); + const dotnetExecutablePath = dotnetInfo.path; + + _channel.appendLine('Dotnet path: ' + dotnetExecutablePath); + + let args: string[] = []; + + if (options.waitForDebugger) { + args.push('--debug'); + } + + const logLevel = options.logLevel; + if (logLevel) { + args.push('--logLevel', logLevel); + } + + if (logLevel && [Trace.Messages, Trace.Verbose].includes(this.GetTraceLevel(logLevel))) { + _channel.appendLine(`Starting server at ${serverPath}`); + } + + // shouldn't this arg only be set if it's running with CSDevKit? + args.push('--telemetryLevel', telemetryReporter.telemetryLevel); + + args.push('--extensionLogDirectory', context.logUri.fsPath); + + let childProcess: cp.ChildProcessWithoutNullStreams; + const cpOptions: cp.SpawnOptionsWithoutStdio = { + detached: true, + windowsHide: true, + env: dotnetInfo.env, + }; + + if (serverPath.endsWith('.dll')) { + // If we were given a path to a dll, launch that via dotnet. + const argsWithPath = [serverPath].concat(args); + + if (logLevel && [Trace.Messages, Trace.Verbose].includes(this.GetTraceLevel(logLevel))) { + _channel.appendLine(`Server arguments ${argsWithPath.join(' ')}`); + } + + childProcess = cp.spawn(dotnetExecutablePath, argsWithPath, cpOptions); + } else { + // Otherwise assume we were given a path to an executable. + if (logLevel && [Trace.Messages, Trace.Verbose].includes(this.GetTraceLevel(logLevel))) { + _channel.appendLine(`Server arguments ${args.join(' ')}`); + } + + childProcess = cp.spawn(serverPath, args, cpOptions); + } + + // Record the stdout and stderr streams from the server process. + childProcess.stdout.on('data', (data: { toString: (arg0: any) => any }) => { + const result: string = isString(data) ? data : data.toString(RoslynLanguageServer.encoding); + _channel.append('[stdout] ' + result); + }); + childProcess.stderr.on('data', (data: { toString: (arg0: any) => any }) => { + const result: string = isString(data) ? data : data.toString(RoslynLanguageServer.encoding); + _channel.append('[stderr] ' + result); + }); + childProcess.on('exit', (code) => { + _channel.appendLine(`Language server process exited with ${code}`); + }); + + // Timeout promise used to time out the connection process if it takes too long. + const timeout = new Promise((resolve) => { + RAL().timer.setTimeout(resolve, options.startTimeout); + }); + + const connectionPromise = new Promise((resolveConnection, rejectConnection) => { + // If the child process exited unexpectedly, reject the promise early. + // Error information will be captured from the stdout/stderr streams above. + childProcess.on('exit', (code) => { + if (code && code !== 0) { + rejectConnection(new Error('Language server process exited unexpectedly')); + } + }); + + // The server process will create the named pipe used for communication. Wait for it to be created, + // and listen for the server to pass back the connection information via stdout. + const namedPipePromise = new Promise((resolve) => { + _channel.appendLine('waiting for named pipe information from server...'); + childProcess.stdout.on('data', (data: { toString: (arg0: any) => any }) => { + const result: string = isString(data) ? data : data.toString(RoslynLanguageServer.encoding); + // Use the regular expression to find all JSON lines + const jsonLines = result.match(RoslynLanguageServer.namedPipeKeyRegex); + if (jsonLines) { + const transmittedPipeNameInfo: NamedPipeInformation = JSON.parse(jsonLines[0]); + _channel.appendLine('received named pipe information from server'); + resolve(transmittedPipeNameInfo); + } + }); + }); + + const socketPromise = namedPipePromise.then(async (pipeConnectionInfo) => { + return new Promise((resolve, reject) => { + _channel.appendLine('attempting to connect client to server...'); + const socket = net.createConnection(pipeConnectionInfo.pipeName, () => { + _channel.appendLine('client has connected to server'); + resolve(socket); + }); + + // If we failed to connect for any reason, ensure the error is propagated. + socket.on('error', (err) => reject(err)); + }); + }); + + socketPromise.then(resolveConnection, rejectConnection); + }); + + // Wait for the client to connect to the named pipe. + let socket: net.Socket | undefined; + if (options.waitForDebugger) { + // Do not timeout the connection when the waitForDebugger option is set. + socket = await connectionPromise; + } else { + socket = await Promise.race([connectionPromise, timeout]); + } + + if (socket === undefined) { + throw new Error('Timeout. Client could not connect to server via named pipe'); + } + + return { + reader: new SocketMessageReader(socket, RoslynLanguageServer.encoding), + writer: new SocketMessageWriter(socket, RoslynLanguageServer.encoding), + }; + } + + private static GetTraceLevel(logLevel: string): Trace { + switch (logLevel) { + case 'Trace': + return Trace.Verbose; + case 'Debug': + return Trace.Messages; + case 'Information': + return Trace.Off; + case 'Warning': + return Trace.Off; + case 'Error': + return Trace.Off; + case 'Critical': + return Trace.Off; + case 'None': + return Trace.Off; + default: + _channel.appendLine( + `Invalid log level ${logLevel}, server will not start. Please set the 'dotnet.server.trace' configuration to a valid value` + ); + throw new Error(`Invalid log level ${logLevel}`); + } + } +} + +/** + * Creates and activates the Roslyn language server. + * The returned promise will complete when the server starts. + */ +export async function activateRoslynLanguageServer( + lspDefinition : RoslynLanguageServerDefinition, + context: vscode.ExtensionContext, + platformInfo: PlatformInformation, + optionObservable: Observable, + outputChannel: vscode.OutputChannel, + reporter: TelemetryReporter, + lspOptions: RoslynLanguageServerOptions +): Promise { + // Create a channel for outputting general logs from the language server. + _channel = outputChannel; + // Create a separate channel for outputting trace logs - these are incredibly verbose and make other logs very difficult to see. + _traceChannel = vscode.window.createOutputChannel(`${lspDefinition.clientName} Trace Logs`); + + const hostExecutableResolver = new DotnetRuntimeExtensionResolver( + platformInfo, + (platformInfo: PlatformInformation) => getServerPath(platformInfo, lspDefinition.serverPathEnvVar, lspDefinition.bundledServerPath, lspOptions), + outputChannel, + context.extensionPath, + context.extension.id, + lspOptions + ); + + const languageServer = await RoslynLanguageServer.initializeAsync( + lspDefinition, + platformInfo, + hostExecutableResolver, + context, + reporter, + lspOptions + ); + + registerCommands(lspDefinition.commandIdPrefix, context, languageServer, hostExecutableResolver, _channel); + + context.subscriptions.push(registerLanguageServerOptionChanges(optionObservable, languageServer, lspOptions, RoslynLanguageOptionsThatTriggerReload)); + + return languageServer; +} + +function getServerPath(platformInfo: PlatformInformation, serverPathEnvVar: string, bundledServerPath : string, options: RoslynLanguageServerOptions) { + let serverPath = process.env[serverPathEnvVar]; + + if (serverPath) { + _channel.appendLine(`Using server path override from ${serverPathEnvVar}: ${serverPath}`); + } else { + serverPath = options.serverPath; + if (!serverPath) { + // Option not set, use the path from the extension. + serverPath = getInstalledServerPath(platformInfo, bundledServerPath); + } + } + + if (!fs.existsSync(serverPath)) { + throw new Error(`Cannot find language server in path '${serverPath}'`); + } + + return serverPath; +} + +function getInstalledServerPath(platformInfo: PlatformInformation, bundledServerPath : string): string { + const clientRoot = __dirname; + const serverFilePath = path.join(clientRoot, bundledServerPath); + + let extension = ''; + if (platformInfo.isWindows()) { + extension = '.exe'; + } else if (platformInfo.isMacOS()) { + // MacOS executables must be signed with codesign. Currently all Roslyn server executables are built on windows + // and therefore dotnet publish does not automatically sign them. + // Tracking bug - https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1767519/ + extension = '.dll'; + } + + let pathWithExtension = `${serverFilePath}${extension}`; + if (!fs.existsSync(pathWithExtension)) { + // We might be running a platform neutral vsix which has no executable, instead we run the dll directly. + pathWithExtension = `${serverFilePath}.dll`; + } + + return pathWithExtension; +} + +export function isString(value: any): value is string { + return typeof value === 'string' || value instanceof String; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynProtocol.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynProtocol.ts new file mode 100644 index 00000000..7b2f57ef --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/roslynProtocol.ts @@ -0,0 +1,25 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/roslynProtocol.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command } from 'vscode'; +import * as lsp from 'vscode-languageserver-protocol'; + +export interface ShowToastNotificationParams { + messageType: lsp.MessageType; + message: string; + commands: Command[]; +} + +export interface NamedPipeInformation { + pipeName: string; +} + +export namespace ShowToastNotification { + export const method = 'window/_roslyn_showToast'; + export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.serverToClient; + export const type = new lsp.NotificationType(method); +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/showToastNotification.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/showToastNotification.ts new file mode 100644 index 00000000..1cb58aa7 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/showToastNotification.ts @@ -0,0 +1,60 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/showToastNotification.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { RoslynLanguageClient } from './roslynLanguageClient'; +import { MessageType } from 'vscode-languageserver-protocol'; +import { ShowToastNotification } from './roslynProtocol'; + +export function registerShowToastNotification(client: RoslynLanguageClient) { + client.onNotification(ShowToastNotification.type, async (notification) => { + const messageOptions: vscode.MessageOptions = { + modal: false, + }; + const commands = notification.commands.map((command) => command.title); + const executeCommandByName = async (result: string | undefined) => { + if (result) { + const command = notification.commands.find((command) => command.title === result); + if (!command) { + throw new Error(`Unknown command ${result}`); + } + + if (command.arguments) { + await vscode.commands.executeCommand(command.command, ...command.arguments); + } else { + await vscode.commands.executeCommand(command.command); + } + } + }; + + switch (notification.messageType) { + case MessageType.Error: { + const result = await vscode.window.showErrorMessage(notification.message, messageOptions, ...commands); + executeCommandByName(result); + break; + } + case MessageType.Warning: { + const result = await vscode.window.showWarningMessage( + notification.message, + messageOptions, + ...commands + ); + executeCommandByName(result); + break; + } + default: { + const result = await vscode.window.showInformationMessage( + notification.message, + messageOptions, + ...commands + ); + executeCommandByName(result); + break; + } + } + }); +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/lsptoolshost/uriConverter.ts b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/uriConverter.ts new file mode 100644 index 00000000..b4c93767 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/lsptoolshost/uriConverter.ts @@ -0,0 +1,36 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/uriConverter.ts +// comment out razor-specific code + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +//import { CSharpProjectedDocumentContentProvider } from '../razor/src/csharp/csharpProjectedDocumentContentProvider'; + +export class UriConverter { + public static serialize(uri: vscode.Uri): string { + /* + if (uri.scheme === CSharpProjectedDocumentContentProvider.scheme) { + // VSCode specifically handles file schemes different than others: + // https://github.com/microsoft/vscode-uri/blob/b54811339bd748982d0e2697fa857a3fecc72522/src/uri.ts#L606 + // Since it's desirable that URIs follow the same scheme across different OSs regardless of + // path separator, cause generation to happen as if it was a file scheme and then replace + // with the actual scheme. This behavior follows the expectations in RazorDynamicFileInfoProvider.cs + const fileSchemUri = uri.with({ scheme: 'file' }); + const uriString = fileSchemUri.toString(true); + return uri.scheme + uriString.slice('file'.length); + } else { + */ + // Fix issue in System.Uri where file:///c%3A/file.txt is not a valid Windows path + return uri.toString(true); + /* + } + */ + } + + public static deserialize(value: string): vscode.Uri { + return vscode.Uri.parse(value); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/main.ts b/msbuild-editor-vscode/src/roslynImport/main.ts new file mode 100644 index 00000000..d31524a2 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/main.ts @@ -0,0 +1,156 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/main.ts +// heavily edited to remove C# specific functionality, comment out unused functionality +// not yet parameterized - hardcodes MSBuild specific paths/ids/etc + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as util from './common'; +import * as vscode from 'vscode'; + +import { ActivationFailure } from './omnisharp/loggingEvents'; +//import { CsharpChannelObserver } from './shared/observers/csharpChannelObserver'; +//import { CsharpLoggerObserver } from './shared/observers/csharpLoggerObserver'; +import { EventStream } from './eventStream'; +import { PlatformInformation } from './shared/platform'; +//import { TelemetryObserver } from './observers/telemetryObserver'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import createOptionStream from './shared/observables/createOptionStream'; +import { RoslynLanguageServer, activateRoslynLanguageServer, RoslynLanguageServerOptions, RoslynLanguageServerDefinition } from './lsptoolshost/roslynLanguageServer'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import path from 'path'; +/* +import { ServerStateChange } from './lsptoolshost/serverStateChange'; +import { languageServerOptions } from './shared/options'; +import { getComponentFolder } from './lsptoolshost/builtInComponents'; +*/ + +export async function activate(context: vscode.ExtensionContext, lspOptions : RoslynLanguageServerOptions, lspDefinition : RoslynLanguageServerDefinition) { + const optionStream = createOptionStream("msbuild"); + + const eventStream = new EventStream(); + + util.setExtensionPath(context.extension.extensionPath); + + let platformInfo: PlatformInformation; + try { + platformInfo = await PlatformInformation.GetCurrent(); + } catch (error) { + eventStream.post(new ActivationFailure()); + throw error; + } + + const aiKey = "80591d65-3815-4ee1-9572-d1b3691a83b1"; // context.extension.packageJSON.contributes.debuggers[0].aiKey; + const reporter = new TelemetryReporter(aiKey); + // ensure it gets properly disposed. Upon disposal the events will be flushed. + context.subscriptions.push(reporter); + + const csharpChannel = vscode.window.createOutputChannel('MSBuild'); + /* + const csharpChannelObserver = new CsharpChannelObserver(csharpChannel); + const csharpLogObserver = new CsharpLoggerObserver(csharpChannel); + eventStream.subscribe(csharpChannelObserver.post); + eventStream.subscribe(csharpLogObserver.post); + + // If the dotnet bundle is installed, this will ensure the dotnet CLI is on the path. + //await initializeDotnetPath(); + + const telemetryObserver = new TelemetryObserver(platformInfo, () => reporter); + eventStream.subscribe(telemetryObserver.post); + + const roslynLanguageServerEvents = new RoslynLanguageServerEvents(); + context.subscriptions.push(roslynLanguageServerEvents); + */ + let roslynLanguageServerStartedPromise: Promise | undefined = undefined; + /* + let projectInitializationCompletePromise: Promise | undefined = undefined; + + // Setup a listener for project initialization complete before we start the server. + projectInitializationCompletePromise = new Promise((resolve, _) => { + roslynLanguageServerEvents.onServerStateChange(async (state) => { + if (state === ServerStateChange.ProjectInitializationComplete) { + resolve(); + } + }); + }); +*/ + // Start the server, but do not await the completion to avoid blocking activation. + roslynLanguageServerStartedPromise = activateRoslynLanguageServer( + lspDefinition, + context, + platformInfo, + optionStream, + csharpChannel, + reporter, + lspOptions + ); + + if (!isSupportedPlatform(platformInfo)) { + let errorMessage = `The ${context.extension.packageJSON.displayName} extension for Visual Studio Code is incompatible on ${platformInfo.platform} ${platformInfo.architecture}`; + await vscode.window.showErrorMessage(errorMessage); + + // Unsupported platform + return null; + } + + reporter.sendTelemetryEvent('MSBuildActivated'); + + // If we got here, the server should definitely have been created. + util.isNotNull(roslynLanguageServerStartedPromise); + //util.isNotNull(projectInitializationCompletePromise); + + /* + const languageServerExport = new RoslynLanguageServerExport(roslynLanguageServerStartedPromise); + return { + initializationFinished: async () => { + await coreClrDebugPromise; + await razorLanguageServerStartedPromise; + await roslynLanguageServerStartedPromise; + await projectInitializationCompletePromise; + }, + profferBrokeredServices: (container) => + profferBrokeredServices(context, container, roslynLanguageServerStartedPromise!), + logDirectory: context.logUri.fsPath, + determineBrowserType: BlazorDebugConfigurationProvider.determineBrowserType, + experimental: { + sendServerRequest: async (t, p, ct) => await languageServerExport.sendRequest(t, p, ct), + languageServerEvents: roslynLanguageServerEvents, + }, + getComponentFolder: (componentName) => { + return getComponentFolder(componentName, languageServerOptions); + }, + }; + */ +} + +function isSupportedPlatform(platform: PlatformInformation): boolean { + if (platform.isWindows()) { + return platform.architecture === 'x86_64' || platform.architecture === 'arm64'; + } + + if (platform.isMacOS()) { + return true; + } + + if (platform.isLinux()) { + return ( + platform.architecture === 'x86_64' || + platform.architecture === 'x86' || + platform.architecture === 'i686' || + platform.architecture === 'arm64' + ); + } + + return false; +} + +/* +async function initializeDotnetPath(): Promise { + const dotnetPackApi = await getDotnetPackApi(); + if (dotnetPackApi !== undefined) { + await dotnetPackApi.getDotnetPath(); + } +} +*/ \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/omnisharp/eventType.ts b/msbuild-editor-vscode/src/roslynImport/omnisharp/eventType.ts new file mode 100644 index 00000000..c0478387 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/omnisharp/eventType.ts @@ -0,0 +1,95 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/omnisharp/eventType.ts +// TODO: cull these + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export enum EventType { + OmnisharpStart = 0, + TelemetryEvent = 1, + TelemetryEventWithMeasures = 2, + OmnisharpDelayTrackerEventMeasures = 3, + OmnisharpInitialisation = 4, + OmnisharpLaunch = 5, + PackageInstallStart = 6, + PackageInstallation = 7, + LogPlatformInfo = 8, + InstallationStart = 9, + InstallationFailure = 10, + DownloadProgress = 11, + OmnisharpFailure = 12, + OmnisharpRequestMessage = 13, + TestExecutionCountReport = 14, + OmnisharpServerOnError = 15, + OmnisharpServerMsBuildProjectDiagnostics = 16, + OmnisharpServerUnresolvedDependencies = 17, + OmnisharpServerEnqueueRequest = 18, + OmnisharpServerProcessRequestStart = 19, + OmnisharpEventPacketReceived = 20, + OmnisharpServerOnServerError = 21, + OmnisharpOnMultipleLaunchTargets = 22, + WorkspaceInformationUpdated = 23, + EventWithMessage = 24, + DownloadStart = 25, + DownloadFallBack = 26, + DownloadSizeObtained = 27, + ZipError = 28, + ReportDotNetTestResults = 29, + DotNetTestRunStart = 30, + DotNetTestDebugStart = 31, + DotNetTestDebugProcessStart = 32, + DotNetTestsInClassRunStart = 33, + DotNetTestsInClassDebugStart = 34, + DocumentSynchronizationFailure = 35, + IntegrityCheckFailure = 37, + IntegrityCheckSuccess = 38, + RazorPluginPathSpecified = 39, + RazorPluginPathDoesNotExist = 40, + DebuggerPrerequisiteFailure = 41, + CommandDotNetRestoreProgress = 42, + DownloadValidation = 43, + DotNetTestDebugComplete = 44, + LatestBuildDownloadStart = 45, + ActiveTextEditorChanged = 46, + OmnisharpOnBeforeServerStart = 47, + ProjectJsonDeprecatedWarning = 48, + OmnisharpServerProcessRequestComplete = 49, + InstallationSuccess = 50, + CommandDotNetRestoreStart = 51, + DebuggerNotInstalledFailure = 52, + ShowOmniSharpChannel = 53, + ActivationFailure = 54, + ProjectModified = 55, + RazorDevModeActive = 56, + DotNetTestDebugStartFailure = 57, + DotNetTestDebugWarning = 58, + DotNetTestRunFailure = 59, + DotNetTestMessage = 60, + OmnisharpServerVerboseMessage = 61, + OmnisharpServerMessage = 62, + OmnisharpServerOnStdErr = 63, + DownloadFailure = 64, + DownloadSuccess = 65, + CommandDotNetRestoreSucceeded = 66, + DebuggerPrerequisiteWarning = 67, + CommandDotNetRestoreFailed = 68, + OmnisharpRestart = 69, + OmnisharpServerDequeueRequest = 70, + OmnisharpServerOnStop = 71, + OmnisharpServerOnStart = 72, + OmnisharpOnBeforeServerInstall = 73, + ProjectConfigurationReceived = 74, + // ProjectDiagnosticStatus = 75, Obsolete, use BackgroundDiagnosticStatus + DotNetTestRunInContextStart = 76, + DotNetTestDebugInContextStart = 77, + TelemetryErrorEvent = 78, + OmnisharpServerRequestCancelled = 79, + BackgroundDiagnosticStatus = 80, + // DevCertCreationFailure = 81, Removed as we push to output channel directly + ShowChannel = 82, +} + +//Note that the EventType protocol is shared with Razor.VSCode and the numbers here should not be altered +//The enum is needed because we use webpack for the extension(which trims the names in production mode) and need to be able to filter on the eventType \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/omnisharp/loggingEvents.ts b/msbuild-editor-vscode/src/roslynImport/omnisharp/loggingEvents.ts new file mode 100644 index 00000000..5bc37ffe --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/omnisharp/loggingEvents.ts @@ -0,0 +1,224 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/omnisharp/loggingEvents.ts +// culled a lot of types we aren't using, more to come + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PlatformInformation } from '../shared/platform'; +import { EventType } from './eventType'; + +export interface BaseEvent { + type: EventType; +} + +export class TelemetryEvent implements BaseEvent { + type = EventType.TelemetryEvent; + constructor( + public eventName: string, + public properties?: { [key: string]: string }, + public measures?: { [key: string]: number } + ) {} +} + +export class TelemetryErrorEvent implements BaseEvent { + type = EventType.TelemetryErrorEvent; + constructor( + public eventName: string, + public properties?: { [key: string]: string }, + public measures?: { [key: string]: number }, + public errorProps?: string[] + ) {} +} + +export class TelemetryEventWithMeasures implements BaseEvent { + type = EventType.TelemetryEvent; + constructor(public eventName: string, public measures: { [key: string]: number }) {} +} + +export class PackageInstallStart implements BaseEvent { + type = EventType.PackageInstallStart; +} + +export class PackageInstallation implements BaseEvent { + type = EventType.PackageInstallation; + constructor(public packageInfo: string) {} +} + +export class LogPlatformInfo implements BaseEvent { + type = EventType.LogPlatformInfo; + constructor(public info: PlatformInformation) {} +} + +export class InstallationStart implements BaseEvent { + type = EventType.InstallationStart; + constructor(public packageDescription: string) {} +} + +export class InstallationFailure implements BaseEvent { + type = EventType.InstallationFailure; + constructor(public stage: string, public error: any) {} +} + +export class DownloadProgress implements BaseEvent { + type = EventType.DownloadProgress; + constructor(public downloadPercentage: number, public packageDescription: string) {} +} + +export class TestExecutionCountReport implements BaseEvent { + type = EventType.TestExecutionCountReport; + constructor( + public debugCounts: { [testFrameworkName: string]: number } | undefined, + public runCounts: { [testFrameworkName: string]: number } | undefined + ) {} +} + +export class EventWithMessage implements BaseEvent { + type = EventType.EventWithMessage; + constructor(public message: string) {} +} + +export class DownloadStart implements BaseEvent { + type = EventType.DownloadStart; + constructor(public packageDescription: string) {} +} + +export class DownloadFallBack implements BaseEvent { + type = EventType.DownloadFallBack; + constructor(public fallbackUrl: string) {} +} + +export class DownloadSizeObtained implements BaseEvent { + type = EventType.DownloadSizeObtained; + constructor(public packageSize: number) {} +} + +export class ZipError implements BaseEvent { + type = EventType.ZipError; + constructor(public message: string) {} +} + +export class DotNetTestRunStart implements BaseEvent { + type = EventType.DotNetTestRunStart; + constructor(public testMethod: string) {} +} + +export class DotNetTestDebugStart implements BaseEvent { + type = EventType.DotNetTestDebugStart; + constructor(public testMethod: string) {} +} + +export class DotNetTestDebugProcessStart implements BaseEvent { + type = EventType.DotNetTestDebugProcessStart; + constructor(public targetProcessId: number) {} +} + +export class DotNetTestsInClassRunStart implements BaseEvent { + type = EventType.DotNetTestsInClassRunStart; + constructor(public className: string) {} +} + +export class DotNetTestsInClassDebugStart implements BaseEvent { + type = EventType.DotNetTestsInClassDebugStart; + constructor(public className: string) {} +} + +export class DotNetTestRunInContextStart implements BaseEvent { + type = EventType.DotNetTestRunInContextStart; + constructor(public fileName: string, public line: number, public column: number) {} +} + +export class DotNetTestDebugInContextStart implements BaseEvent { + type = EventType.DotNetTestDebugInContextStart; + constructor(public fileName: string, public line: number, public column: number) {} +} + +export class DocumentSynchronizationFailure implements BaseEvent { + type = EventType.DocumentSynchronizationFailure; + constructor(public documentPath: string, public errorMessage: string) {} +} + +export class IntegrityCheckFailure { + type = EventType.IntegrityCheckFailure; + constructor(public packageDescription: string, public url: string, public retry: boolean) {} +} + +export class IntegrityCheckSuccess { + type = EventType.IntegrityCheckSuccess; +} + +export class RazorPluginPathSpecified implements BaseEvent { + type = EventType.RazorPluginPathSpecified; + constructor(public path: string) {} +} + +export class RazorPluginPathDoesNotExist implements BaseEvent { + type = EventType.RazorPluginPathDoesNotExist; + constructor(public path: string) {} +} + +export class DebuggerPrerequisiteFailure extends EventWithMessage { + type = EventType.DebuggerPrerequisiteFailure; +} +export class DebuggerPrerequisiteWarning extends EventWithMessage { + type = EventType.DebuggerPrerequisiteWarning; +} +export class CommandDotNetRestoreProgress extends EventWithMessage { + type = EventType.CommandDotNetRestoreProgress; +} +export class CommandDotNetRestoreSucceeded extends EventWithMessage { + type = EventType.CommandDotNetRestoreSucceeded; +} +export class CommandDotNetRestoreFailed extends EventWithMessage { + type = EventType.CommandDotNetRestoreFailed; +} +export class DownloadSuccess extends EventWithMessage { + type = EventType.DownloadSuccess; +} +export class DownloadFailure extends EventWithMessage { + type = EventType.DownloadFailure; +} +export class DotNetTestMessage extends EventWithMessage { + type = EventType.DotNetTestMessage; +} +export class DotNetTestRunFailure extends EventWithMessage { + type = EventType.DotNetTestRunFailure; +} +export class DotNetTestDebugWarning extends EventWithMessage { + type = EventType.DotNetTestDebugWarning; +} +export class DotNetTestDebugStartFailure extends EventWithMessage { + type = EventType.DotNetTestDebugStartFailure; +} + +export class RazorDevModeActive implements BaseEvent { + type = EventType.RazorDevModeActive; +} +export class ProjectModified implements BaseEvent { + type = EventType.ProjectModified; +} +export class ActivationFailure implements BaseEvent { + type = EventType.ActivationFailure; +} +export class ShowOmniSharpChannel implements BaseEvent { + type = EventType.ShowOmniSharpChannel; +} +export class DebuggerNotInstalledFailure implements BaseEvent { + type = EventType.DebuggerNotInstalledFailure; +} +export class CommandDotNetRestoreStart implements BaseEvent { + type = EventType.CommandDotNetRestoreStart; +} +export class InstallationSuccess implements BaseEvent { + type = EventType.InstallationSuccess; +} +export class DotNetTestDebugComplete implements BaseEvent { + type = EventType.DotNetTestDebugComplete; +} +export class DownloadValidation implements BaseEvent { + type = EventType.DownloadValidation; +} +export class ShowChannel implements BaseEvent { + type = EventType.ShowChannel; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/packageManager/absolutePath.ts b/msbuild-editor-vscode/src/roslynImport/packageManager/absolutePath.ts new file mode 100644 index 00000000..0dbd0d2c --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/packageManager/absolutePath.ts @@ -0,0 +1,20 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/packageManager/absolutePath.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; + +export class AbsolutePath { + constructor(public value: string) { + if (!path.isAbsolute(value)) { + throw new Error('The path must be absolute'); + } + } + + public static getAbsolutePath(...pathSegments: string[]): AbsolutePath { + return new AbsolutePath(path.resolve(...pathSegments)); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/constants/IHostExecutableResolver.ts b/msbuild-editor-vscode/src/roslynImport/shared/constants/IHostExecutableResolver.ts new file mode 100644 index 00000000..a495c761 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/constants/IHostExecutableResolver.ts @@ -0,0 +1,12 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/constants/IHostExecutableResolver.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HostExecutableInformation } from './hostExecutableInformation'; + +export interface IHostExecutableResolver { + getHostExecutableInfo(): Promise; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/constants/hostExecutableInformation.ts b/msbuild-editor-vscode/src/roslynImport/shared/constants/hostExecutableInformation.ts new file mode 100644 index 00000000..671b1ad5 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/constants/hostExecutableInformation.ts @@ -0,0 +1,12 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/constants/hostExecutableInformation.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface HostExecutableInformation { + version: string; + path: string; + env: NodeJS.ProcessEnv; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/observables/createOptionStream.ts b/msbuild-editor-vscode/src/roslynImport/shared/observables/createOptionStream.ts new file mode 100644 index 00000000..fd1d06de --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/observables/createOptionStream.ts @@ -0,0 +1,31 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/shared/observables/createOptionStream.ts +// parameterized the configuration name, removed the adapter + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//import { vscode } from '../../vscodeAdapter'; +import * as vscode from 'vscode'; +import { Observable, Observer } from 'rxjs'; +import { publishBehavior } from 'rxjs/operators'; + +export default function createOptionStream(/*vscode: vscode*/ observedConfigurationName : string): Observable { + return Observable.create((observer: Observer) => { + const disposable = vscode.workspace.onDidChangeConfiguration((e) => { + //if the observed are affected only then read the options + if ( e.affectsConfiguration(observedConfigurationName)) { + observer.next(); + } + }); + + return () => disposable.dispose(); + }) + .pipe( + publishBehavior(() => { + return; + }) + ) + .refCount(); +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/observers/optionChangeObserver.ts b/msbuild-editor-vscode/src/roslynImport/shared/observers/optionChangeObserver.ts new file mode 100644 index 00000000..1672de9d --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/observers/optionChangeObserver.ts @@ -0,0 +1,44 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/shared/observers/optionChangeObserver.ts +// made generic and simplified to only deal with one option type + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Observable } from 'rxjs'; +import Disposable from '../../disposable'; +import { isDeepStrictEqual } from 'util'; + +export function HandleOptionChanges( + optionObservable: Observable, + optionChangeObserver: OptionChangeObserver, + options : TOptions +): Disposable { + let oldRelevantOptions: Map; + const subscription = optionObservable.pipe().subscribe(() => { + const relevantKeys = optionChangeObserver.getRelevantOptions(); + const newRelevantOptions = new Map(relevantKeys.map((key) => [key, options[key]])); + + if (!oldRelevantOptions) { + oldRelevantOptions = newRelevantOptions; + } + + const changedRelevantOptions = relevantKeys.filter( + (key) => !isDeepStrictEqual(oldRelevantOptions.get(key), newRelevantOptions.get(key)) + ); + + oldRelevantOptions = newRelevantOptions; + + if (changedRelevantOptions.length > 0) { + optionChangeObserver.handleOptionChanges(changedRelevantOptions); + } + }); + + return new Disposable(subscription); +} + +export interface OptionChangeObserver { + getRelevantOptions: () => ReadonlyArray; + handleOptionChanges: (optionChanges: ReadonlyArray) => void; +} diff --git a/msbuild-editor-vscode/src/roslynImport/shared/options.ts b/msbuild-editor-vscode/src/roslynImport/shared/options.ts new file mode 100644 index 00000000..bce888be --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/options.ts @@ -0,0 +1,46 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/89da0f69901965627158f0120e1273ea2fc2486b/src/shared/options.ts +// removed all the option definitions, now only has helpers + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +function getExcludedPaths(): string[] { + const workspaceConfig = vscode.workspace.getConfiguration(); + + const excludePaths = getExcludes(workspaceConfig, 'files.exclude'); + return excludePaths; + + function getExcludes(config: vscode.WorkspaceConfiguration, option: string): string[] { + const optionValue = config.get<{ [i: string]: boolean }>(option, {}); + return Object.entries(optionValue) + .filter(([_, value]) => value) + .map(([key, _]) => key); + } +} + +/** + * Reads an option from the vscode config with an optional back compat parameter. + */ +function readOptionFromConfig( + config: vscode.WorkspaceConfiguration, + option: string, + defaultValue: T, + ...backCompatOptionNames: string[] +): T { + let value = config.get(option); + + if (value === undefined && backCompatOptionNames.length > 0) { + // Search the back compat options for a defined value. + value = backCompatOptionNames.map((name) => config.get(name)).find((val) => val); + } + + return value ?? defaultValue; +} + +export function readOption(option: string, defaultValue: T, ...backCompatOptionNames: string[]): T { + return readOptionFromConfig(vscode.workspace.getConfiguration(), option, defaultValue, ...backCompatOptionNames); +} diff --git a/msbuild-editor-vscode/src/roslynImport/shared/platform.ts b/msbuild-editor-vscode/src/roslynImport/shared/platform.ts new file mode 100644 index 00000000..46c07291 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/platform.ts @@ -0,0 +1,198 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/platform.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as util from '../common'; + +const unknown = 'unknown'; + +/** + * There is no standard way on Linux to find the distribution name and version. + * Recently, systemd has pushed to standardize the os-release file. This has + * seen adoption in "recent" versions of all major distributions. + * https://www.freedesktop.org/software/systemd/man/os-release.html + */ +export class LinuxDistribution { + public constructor(public name: string, public version: string) {} + + public static async GetCurrent(): Promise { + // Try /etc/os-release and fallback to /usr/lib/os-release per the synopsis + // at https://www.freedesktop.org/software/systemd/man/os-release.html. + return LinuxDistribution.FromFilePath('/etc/os-release') + .catch(async () => LinuxDistribution.FromFilePath('/usr/lib/os-release')) + .catch(async () => Promise.resolve(new LinuxDistribution(unknown, unknown))); + } + + public toString(): string { + return `name=${this.name}, version=${this.version}`; + } + + /** + * Returns a string representation of LinuxDistribution that only returns the + * distro name if it appears on an allowed list of known distros. Otherwise, + * it returns 'other'. + */ + public toTelemetryString(): string { + const allowedList = [ + 'alpine', + 'antergos', + 'arch', + 'centos', + 'debian', + 'deepin', + 'elementary', + 'fedora', + 'galliumos', + 'gentoo', + 'kali', + 'linuxmint', + 'manjoro', + 'neon', + 'opensuse', + 'parrot', + 'rhel', + 'ubuntu', + 'zorin', + ]; + + if (this.name === unknown || allowedList.indexOf(this.name) >= 0) { + return this.toString(); + } else { + // Having a hash of the name will be helpful to identify spikes in the 'other' + // bucket when a new distro becomes popular and needs to be added to the + // allowed list above. + const hash = crypto.createHash('sha256'); + hash.update(this.name); + + const hashedName = hash.digest('hex'); + + return `other (${hashedName})`; + } + } + + private static async FromFilePath(filePath: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (error, data) => { + if (error) { + reject(error); + } else { + resolve(LinuxDistribution.FromReleaseInfo(data)); + } + }); + }); + } + + public static FromReleaseInfo(releaseInfo: string, eol: string = os.EOL): LinuxDistribution { + let name = unknown; + let version = unknown; + + const lines = releaseInfo.split(eol); + for (let line of lines) { + line = line.trim(); + + const equalsIndex = line.indexOf('='); + if (equalsIndex >= 0) { + const key = line.substring(0, equalsIndex); + let value = line.substring(equalsIndex + 1); + + // Strip double quotes if necessary + if (value.length > 1 && value.startsWith('"') && value.endsWith('"')) { + value = value.substring(1, value.length - 1); + } + + if (key === 'ID') { + name = value; + } else if (key === 'VERSION_ID') { + version = value; + } + + if (name !== unknown && version !== unknown) { + break; + } + } + } + + return new LinuxDistribution(name, version); + } +} + +export class PlatformInformation { + public constructor(public platform: string, public architecture: string, public distribution?: LinuxDistribution) {} + + public isWindows(): boolean { + return this.platform === 'win32'; + } + + public isMacOS(): boolean { + return this.platform === 'darwin'; + } + + public isLinux(): boolean { + return this.platform.startsWith('linux'); + } + + public toString(): string { + let result = `${this.platform}, ${this.architecture}`; + if (this.distribution !== undefined) { + result += `, ${this.distribution.toString()}`; + } + + return result; + } + + public static async GetCurrent(): Promise { + const platform = os.platform(); + if (platform === 'win32') { + return new PlatformInformation(platform, PlatformInformation.GetWindowsArchitecture()); + } else if (platform === 'darwin') { + return new PlatformInformation(platform, await PlatformInformation.GetUnixArchitecture()); + } else if (platform === 'linux') { + const [isMusl, architecture, distribution] = await Promise.all([ + PlatformInformation.GetIsMusl(), + PlatformInformation.GetUnixArchitecture(), + LinuxDistribution.GetCurrent(), + ]); + return new PlatformInformation(isMusl ? 'linux-musl' : platform, architecture, distribution); + } + + throw new Error(`Unsupported platform: ${platform}`); + } + + private static GetWindowsArchitecture(): string { + if (process.env.PROCESSOR_ARCHITECTURE === 'x86' && process.env.PROCESSOR_ARCHITEW6432 === undefined) { + return 'x86'; + } else if (process.env.PROCESSOR_ARCHITECTURE === 'ARM64' && process.env.PROCESSOR_ARCHITEW6432 === undefined) { + return 'arm64'; + } else { + return 'x86_64'; + } + } + + private static async GetUnixArchitecture(): Promise { + const architecture = (await util.execChildProcess('uname -m', __dirname)).trim(); + if (architecture === 'aarch64') { + return 'arm64'; + } + return architecture; + } + + // Emulates https://github.com/dotnet/install-scripts/blob/3c6cc06/src/dotnet-install.sh#L187-L189. + private static async GetIsMusl(): Promise { + try { + const output = await util.execChildProcess('ldd --version', __dirname); + return output.includes('musl'); + } catch (err) { + return err instanceof Error ? err.message.includes('musl') : false; + } + } + + public isValidPlatformForMono(): boolean { + return this.isLinux() || this.isMacOS(); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/reportIssue.ts b/msbuild-editor-vscode/src/roslynImport/shared/reportIssue.ts new file mode 100644 index 00000000..3c35f489 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/reportIssue.ts @@ -0,0 +1,113 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/reportIssue.ts +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/lsptoolshost/commands.ts +// parameterized extension-specific info to make cleanly reusable + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver'; +import { basename, dirname } from 'path'; +import { DotnetInfo } from './utils/dotnetInfo'; + +export default async function reportIssue( + context: vscode.ExtensionContext, + lspTraceOptionId : string, + getDotnetInfo: (dotNetCliPaths: string[]) => Promise, + dotnetResolver: IHostExecutableResolver +) { + const extensionId : string = context.extension.id; + const extensionVersion : string = context.extension.packageJSON.version; + const extensionName : string = context.extension.packageJSON.displayName; + + // Get info for the dotnet that the language server executable is run on, not the dotnet the language server will execute user code on. + let fullDotnetInfo: string | undefined; + try { + const info = await dotnetResolver.getHostExecutableInfo(); + const dotnetInfo = await getDotnetInfo([dirname(info.path)]); + fullDotnetInfo = dotnetInfo.FullInfo; + } catch (error) { + const message = error instanceof Error ? error.message : `${error}`; + fullDotnetInfo = message; + } + + const extensions = getInstalledExtensions(); + + const body = `## Issue Description ## +## Steps to Reproduce ## + +## Expected Behavior ## + +## Actual Behavior ## + +## Logs ## + + + +### C# log ### +

Post the output from Output-->${extensionName} here
+ +### C# LSP Trace Logs ### +
Post the output from Output-->${extensionName} LSP Trace Logs here. Requires \`${lspTraceOptionId}\` to be set to \`Trace\`
+ +## Environment information ## + +**VSCode version**: ${vscode.version} +**${extensionName} version**: ${extensionVersion} + +
Dotnet Information +${fullDotnetInfo}
+
Visual Studio Code Extensions +${generateExtensionTable(extensions)} +
+`; + + await vscode.commands.executeCommand('workbench.action.openIssueReporter', { + extensionId: extensionId, + issueBody: body, + }); +} + +function sortExtensions(a: vscode.Extension, b: vscode.Extension): number { + if (a.packageJSON.name.toLowerCase() < b.packageJSON.name.toLowerCase()) { + return -1; + } + if (a.packageJSON.name.toLowerCase() > b.packageJSON.name.toLowerCase()) { + return 1; + } + return 0; +} + +function generateExtensionTable(extensions: vscode.Extension[]) { + if (extensions.length <= 0) { + return 'none'; + } + + const tableHeader = `|Extension|Author|Version|Folder Name|\n|---|---|---|---|`; + const table = extensions + .map( + (e) => + `|${e.packageJSON.name}|${e.packageJSON.publisher}|${e.packageJSON.version}|${basename( + e.extensionPath + )}|` + ) + .join('\n'); + + const extensionTable = ` +${tableHeader}\n${table}; +`; + + return extensionTable; +} + +function getInstalledExtensions() { + const extensions = vscode.extensions.all.filter((extension) => extension.packageJSON.isBuiltin === false); + + return extensions.sort(sortExtensions); +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/utils/dotnetInfo.ts b/msbuild-editor-vscode/src/roslynImport/shared/utils/dotnetInfo.ts new file mode 100644 index 00000000..d131a038 --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/utils/dotnetInfo.ts @@ -0,0 +1,24 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/utils/dotnetInfo.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as semver from 'semver'; + +type RuntimeVersionMap = { [runtime: string]: RuntimeInfo[] }; +export interface DotnetInfo { + CliPath?: string; + FullInfo: string; + Version: string; + /* a runtime-only install of dotnet will not output a runtimeId in dotnet --info. */ + RuntimeId?: string; + Architecture?: string; + Runtimes: RuntimeVersionMap; +} + +export interface RuntimeInfo { + Version: semver.SemVer; + Path: string; +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/roslynImport/shared/utils/getDotnetInfo.ts b/msbuild-editor-vscode/src/roslynImport/shared/utils/getDotnetInfo.ts new file mode 100644 index 00000000..64a6d60d --- /dev/null +++ b/msbuild-editor-vscode/src/roslynImport/shared/utils/getDotnetInfo.ts @@ -0,0 +1,116 @@ +// https://raw.githubusercontent.com/dotnet/vscode-csharp/ba156937926e760759a7e5e13bcf2d1be8729dc8/src/shared/utils/getDotnetInfo.ts + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as semver from 'semver'; +import { join } from 'path'; +import { execChildProcess } from '../../common'; +import { CoreClrDebugUtil } from '../../coreclrDebug/util'; +import { DotnetInfo, RuntimeInfo } from './dotnetInfo'; +import { EOL } from 'os'; + +// This function calls `dotnet --info` and returns the result as a DotnetInfo object. +export async function getDotnetInfo(dotNetCliPaths: string[]): Promise { + const dotnetExecutablePath = getDotNetExecutablePath(dotNetCliPaths); + + const data = await runDotnetInfo(dotnetExecutablePath); + const dotnetInfo = await parseDotnetInfo(data, dotnetExecutablePath); + return dotnetInfo; +} + +export function getDotNetExecutablePath(dotNetCliPaths: string[]): string | undefined { + const dotnetExeName = `dotnet${CoreClrDebugUtil.getPlatformExeExtension()}`; + let dotnetExecutablePath: string | undefined; + + for (const dotnetPath of dotNetCliPaths) { + const dotnetFullPath = join(dotnetPath, dotnetExeName); + if (CoreClrDebugUtil.existsSync(dotnetFullPath)) { + dotnetExecutablePath = dotnetFullPath; + break; + } + } + return dotnetExecutablePath; +} + +async function runDotnetInfo(dotnetExecutablePath: string | undefined): Promise { + try { + const env = { + ...process.env, + DOTNET_CLI_UI_LANGUAGE: 'en-US', + }; + const command = dotnetExecutablePath ? `"${dotnetExecutablePath}"` : 'dotnet'; + const data = await execChildProcess(`${command} --info`, process.cwd(), env); + return data; + } catch (error) { + const message = error instanceof Error ? error.message : `${error}`; + throw new Error(`Error running dotnet --info: ${message}`); + } +} + +async function parseDotnetInfo(dotnetInfo: string, dotnetExecutablePath: string | undefined): Promise { + try { + const cliPath = dotnetExecutablePath; + const fullInfo = dotnetInfo; + + let version: string | undefined; + let runtimeId: string | undefined; + let architecture: string | undefined; + + let lines = dotnetInfo.replace(/\r/gm, '').split('\n'); + for (const line of lines) { + let match: RegExpMatchArray | null; + if ((match = /^\s*Version:\s*([^\s].*)$/.exec(line))) { + version = match[1]; + } else if ((match = /^ RID:\s*([\w\-.]+)$/.exec(line))) { + runtimeId = match[1]; + } else if ((match = /^\s*Architecture:\s*(.*)/.exec(line))) { + architecture = match[1]; + } + } + + const runtimeVersions: { [runtime: string]: RuntimeInfo[] } = {}; + const command = dotnetExecutablePath ? `"${dotnetExecutablePath}"` : 'dotnet'; + const listRuntimes = await execChildProcess(`${command} --list-runtimes`, process.cwd(), process.env); + lines = listRuntimes.split(/\r?\n/); + for (const line of lines) { + let match: RegExpMatchArray | null; + if ((match = /^([\w.]+) ([^ ]+) \[([^\]]+)\]$/.exec(line))) { + const runtime = match[1]; + const runtimeVersion = match[2]; + if (runtime in runtimeVersions) { + runtimeVersions[runtime].push({ + Version: semver.parse(runtimeVersion)!, + Path: match[3], + }); + } else { + runtimeVersions[runtime] = [ + { + Version: semver.parse(runtimeVersion)!, + Path: match[3], + }, + ]; + } + } + } + + if (version !== undefined) { + const dotnetInfo: DotnetInfo = { + CliPath: cliPath, + FullInfo: fullInfo, + Version: version, + RuntimeId: runtimeId, + Architecture: architecture, + Runtimes: runtimeVersions, + }; + return dotnetInfo; + } + + throw new Error('Failed to parse dotnet version information'); + } catch (error) { + const message = error instanceof Error ? error.message : `${error}`; + throw new Error(`Error parsing dotnet --info: ${message}, raw info was:${EOL}${dotnetInfo}`); + } +} \ No newline at end of file diff --git a/msbuild-editor-vscode/src/test/extension.test.ts b/msbuild-editor-vscode/src/test/extension.test.ts new file mode 100644 index 00000000..4ca0ab41 --- /dev/null +++ b/msbuild-editor-vscode/src/test/extension.test.ts @@ -0,0 +1,15 @@ +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +// import * as myExtension from '../../extension'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/MonoDevelop.MSBuildEditor/Syntax/OSSREADME.json b/msbuild-editor-vscode/syntaxes/OSSREADME.json similarity index 100% rename from MonoDevelop.MSBuildEditor/Syntax/OSSREADME.json rename to msbuild-editor-vscode/syntaxes/OSSREADME.json diff --git a/MonoDevelop.MSBuildEditor/Syntax/msbuild.json b/msbuild-editor-vscode/syntaxes/msbuild.tmLanguage.json similarity index 99% rename from MonoDevelop.MSBuildEditor/Syntax/msbuild.json rename to msbuild-editor-vscode/syntaxes/msbuild.tmLanguage.json index 3a231c3c..2ad38a45 100644 --- a/MonoDevelop.MSBuildEditor/Syntax/msbuild.json +++ b/msbuild-editor-vscode/syntaxes/msbuild.tmLanguage.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/Septh/tmlanguage/master/tmLanguage.schema.json", + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "scopeName": "text.msbuild", "name": "MSBuild", "uuid": "4675e0d6-7db4-4036-8b75-b53c51464b17", diff --git a/msbuild-editor-vscode/tsconfig.json b/msbuild-editor-vscode/tsconfig.json new file mode 100644 index 00000000..8a79f20f --- /dev/null +++ b/msbuild-editor-vscode/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": [ + "ES2022" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + } +}