diff --git a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs index 950506809..caa66b332 100644 --- a/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Definitions/IModuleResolution.cs @@ -13,6 +13,7 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Python.Analysis.Core.DependencyResolution; @@ -50,6 +51,12 @@ public interface IModuleResolution { /// IPythonModule GetImportedModule(string name); + /// + /// Sets user search paths. This changes . + /// + /// Added roots. + IEnumerable SetUserSearchPaths(in IEnumerable searchPaths); + Task ReloadAsync(CancellationToken token = default); } } diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs index 41f1a2d2f..039736b04 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs @@ -147,25 +147,25 @@ internal async Task LoadBuiltinTypesAsync(CancellationToken cancellationToken = var builtinModuleNamesMember = BuiltinsModule.GetAnyMember("__builtin_module_names__"); if (builtinModuleNamesMember.TryGetConstant(out var s)) { var builtinModuleNames = s.Split(',').Select(n => n.Trim()); - _pathResolver.SetBuiltins(builtinModuleNames); + PathResolver.SetBuiltins(builtinModuleNames); } } public override async Task ReloadAsync(CancellationToken cancellationToken = default) { ModuleCache = new ModuleCache(_interpreter, _services); - _pathResolver = new PathResolver(_interpreter.LanguageVersion); + PathResolver = new PathResolver(_interpreter.LanguageVersion); - var addedRoots = _pathResolver.SetRoot(_root); + var addedRoots = PathResolver.SetRoot(_root); ReloadModulePaths(addedRoots); var interpreterPaths = await GetSearchPathsAsync(cancellationToken); - addedRoots = _pathResolver.SetInterpreterSearchPaths(interpreterPaths); + addedRoots = PathResolver.SetInterpreterSearchPaths(interpreterPaths); ReloadModulePaths(addedRoots); cancellationToken.ThrowIfCancellationRequested(); - addedRoots = _pathResolver.SetUserSearchPaths(_interpreter.Configuration.SearchPaths); + addedRoots = SetUserSearchPaths(_interpreter.Configuration.SearchPaths); ReloadModulePaths(addedRoots); } diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs b/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs index d7e01b06d..b46a3b0ca 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/ModuleResolutionBase.cs @@ -38,7 +38,7 @@ internal abstract class ModuleResolutionBase { protected readonly bool _requireInitPy; protected string _root; - protected PathResolver _pathResolver; + protected PathResolver PathResolver { get; set; } protected InterpreterConfiguration Configuration => _interpreter.Configuration; @@ -58,7 +58,7 @@ protected ModuleResolutionBase(string root, IServiceContainer services) { /// /// Path resolver providing file resolution in module imports. /// - public PathResolverSnapshot CurrentPathResolver => _pathResolver.CurrentSnapshot; + public PathResolverSnapshot CurrentPathResolver => PathResolver.CurrentSnapshot; /// /// Builtins module. @@ -68,7 +68,7 @@ protected ModuleResolutionBase(string root, IServiceContainer services) { public abstract Task ReloadAsync(CancellationToken cancellationToken = default); protected abstract Task DoImportAsync(string name, CancellationToken cancellationToken = default); - public IReadOnlyCollection GetPackagesFromDirectory(string searchPath, CancellationToken cancellationToken) { + public IReadOnlyCollection GetPackagesFromDirectory(string searchPath, CancellationToken cancellationToken) { return ModulePath.GetModulesInPath( searchPath, recurse: false, @@ -77,10 +77,18 @@ public IReadOnlyCollection GetPackagesFromDirectory(string searchPath, C ).Select(mp => mp.ModuleName).Where(n => !string.IsNullOrEmpty(n)).TakeWhile(_ => !cancellationToken.IsCancellationRequested).ToList(); } - public IPythonModule GetImportedModule(string name) - => _modules.TryGetValue(name, out var module) ? module : null; + public IPythonModule GetImportedModule(string name) { + var module = _interpreter.ModuleResolution.GetSpecializedModule(name); + if (module != null) { + return module; + } + return _modules.TryGetValue(name, out module) ? module : null; + } + + public IEnumerable SetUserSearchPaths(in IEnumerable searchPaths) + => PathResolver.SetUserSearchPaths(searchPaths); - public void AddModulePath(string path) => _pathResolver.TryAddModulePath(path, out var _); + public void AddModulePath(string path) => PathResolver.TryAddModulePath(path, out var _); public ModulePath FindModule(string filePath) { var bestLibraryPath = string.Empty; @@ -207,7 +215,7 @@ private async Task TryImportModuleAsync(string name, Canc protected void ReloadModulePaths(in IEnumerable rootPaths) { foreach (var modulePath in rootPaths.Where(Directory.Exists).SelectMany(p => PathUtils.EnumerateFiles(p))) { - _pathResolver.TryAddModulePath(modulePath, out _); + PathResolver.TryAddModulePath(modulePath, out _); } } diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs index ef8850ff1..51376d6d6 100644 --- a/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs +++ b/src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs @@ -65,12 +65,12 @@ protected override async Task DoImportAsync(string name, Cancella } public override Task ReloadAsync(CancellationToken cancellationToken = default) { - _pathResolver = new PathResolver(_interpreter.LanguageVersion); + PathResolver = new PathResolver(_interpreter.LanguageVersion); - var addedRoots = _pathResolver.SetRoot(_root); + var addedRoots = PathResolver.SetRoot(_root); ReloadModulePaths(addedRoots); - addedRoots = _pathResolver.SetInterpreterSearchPaths(_typeStubPaths); + addedRoots = PathResolver.SetInterpreterSearchPaths(_typeStubPaths); ReloadModulePaths(addedRoots); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Analysis/Ast/Test/AnalysisTestBase.cs b/src/Analysis/Ast/Test/AnalysisTestBase.cs index 3d69856dc..15a2795c7 100644 --- a/src/Analysis/Ast/Test/AnalysisTestBase.cs +++ b/src/Analysis/Ast/Test/AnalysisTestBase.cs @@ -100,6 +100,12 @@ protected async Task CreateServicesAsync(string root, Interpret return sm; } + protected async Task CreateServicesAsync(InterpreterConfiguration configuration, string modulePath) { + modulePath = modulePath ?? TestData.GetDefaultModulePath(); + var moduleDirectory = Path.GetDirectoryName(modulePath); + await CreateServicesAsync(moduleDirectory, configuration); + } + protected Task GetAnalysisAsync(string code, PythonLanguageVersion version, string modulePath = null) => GetAnalysisAsync(code, PythonVersions.GetRequiredCPythonConfiguration(version), modulePath); @@ -129,7 +135,7 @@ protected async Task GetAnalysisAsync( string modulePath = null) { var moduleUri = modulePath != null ? new Uri(modulePath) : TestData.GetDefaultModuleUri(); - modulePath = modulePath ?? TestData .GetDefaultModulePath(); + modulePath = modulePath ?? TestData.GetDefaultModulePath(); moduleName = moduleName ?? Path.GetFileNameWithoutExtension(modulePath); IDocument doc; diff --git a/src/LanguageServer/Impl/Completion/ImportCompletion.cs b/src/LanguageServer/Impl/Completion/ImportCompletion.cs index 3b458dc68..66131c709 100644 --- a/src/LanguageServer/Impl/Completion/ImportCompletion.cs +++ b/src/LanguageServer/Impl/Completion/ImportCompletion.cs @@ -158,8 +158,6 @@ private static CompletionResult GetResultFromSearch(IImportSearchResult importSe break; case PackageImport packageImports: return new CompletionResult(packageImports.Modules - .Select(m => mres.GetImportedModule(m.FullName)) - .ExcludeDefault() .Select(m => CompletionItemSource.CreateCompletionItem(m.Name, CompletionItemKind.Module)) .Prepend(CompletionItemSource.Star)); default: diff --git a/src/LanguageServer/Test/CompletionTests.cs b/src/LanguageServer/Test/CompletionTests.cs index f5a83b82a..e09d0548c 100644 --- a/src/LanguageServer/Test/CompletionTests.cs +++ b/src/LanguageServer/Test/CompletionTests.cs @@ -53,8 +53,8 @@ def method(self): "; var analysis = await GetAnalysisAsync(code); var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); - var comps = (await cs.GetCompletionsAsync(analysis, new SourceLocation(8, 1))).Completions.ToArray(); - comps.Select(c => c.label).Should().Contain("C", "x", "y", "while", "for", "yield"); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(8, 1)); + comps.Should().HaveLabels("C", "x", "y", "while", "for", "yield"); } [TestMethod, Priority(0)] @@ -65,8 +65,8 @@ public async Task StringMembers() { "; var analysis = await GetAnalysisAsync(code); var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); - var comps = (await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 3))).Completions.ToArray(); - comps.Select(c => c.label).Should().Contain(new[] { @"isupper", @"capitalize", @"split" }); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 3)); + comps.Should().HaveLabels(@"isupper", @"capitalize", @"split" ); } [TestMethod, Priority(0)] @@ -77,8 +77,8 @@ import datetime "; var analysis = await GetAnalysisAsync(code); var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); - var comps = (await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 19))).Completions.ToArray(); - comps.Select(c => c.label).Should().Contain(new[] { "now", @"tzinfo", @"ctime" }); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 19)); + comps.Should().HaveLabels("now", @"tzinfo", @"ctime"); } [TestMethod, Priority(0)] @@ -92,11 +92,11 @@ def method1(self): pass "; var analysis = await GetAnalysisAsync(code); var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); - var comps = (await cs.GetCompletionsAsync(analysis, new SourceLocation(5, 4))).Completions.ToArray(); - comps.Select(c => c.label).Should().Contain(@"ABCDE"); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(5, 4)); + comps.Should().HaveLabels(@"ABCDE"); - comps = (await cs.GetCompletionsAsync(analysis, new SourceLocation(6, 9))).Completions.ToArray(); - comps.Select(c => c.label).Should().Contain("method1"); + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(6, 9)); + comps.Should().HaveLabels("method1"); } [DataRow(PythonLanguageVersion.V36, "value")] diff --git a/src/LanguageServer/Test/ImportsTests.cs b/src/LanguageServer/Test/ImportsTests.cs new file mode 100644 index 000000000..82ad89b28 --- /dev/null +++ b/src/LanguageServer/Test/ImportsTests.cs @@ -0,0 +1,301 @@ +// Copyright(c) Microsoft Corporation +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the License); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +// MERCHANTABILITY OR NON-INFRINGEMENT. +// +// See the Apache Version 2.0 License for specific language governing +// permissions and limitations under the License. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis; +using Microsoft.Python.Analysis.Documents; +using Microsoft.Python.Core.Text; +using Microsoft.Python.LanguageServer.Completion; +using Microsoft.Python.LanguageServer.Sources; +using Microsoft.Python.LanguageServer.Tests.FluentAssertions; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.LanguageServer.Tests { + [TestClass] + public class ImportsTests : LanguageServerTestBase { + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [TestMethod, Priority(0)] + public async Task ExplicitImplicitPackageMix() { + const string appCode = @" +import projectA.foo +import projectA.foo.bar +import projectB.foo +import projectB.foo.baz + +projectA."; + + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + var init1Path = Path.Combine(root, "projectA", "foo", "bar", "__init__.py"); + var init2Path = Path.Combine(root, "projectA", "foo", "__init__.py"); + var init3Path = Path.Combine(root, "projectB", "foo", "bar", "__init__.py"); + var init4Path = Path.Combine(root, "projectB", "foo", "__init__.py"); + + await CreateServicesAsync(PythonVersions.LatestAvailable3X, appPath); + var rdt = Services.GetService(); + + rdt.OpenDocument(new Uri(init1Path), string.Empty); + rdt.OpenDocument(new Uri(init2Path), string.Empty); + rdt.OpenDocument(new Uri(init3Path), string.Empty); + rdt.OpenDocument(new Uri(init4Path), string.Empty); + + var doc = rdt.OpenDocument(new Uri(appPath), appCode, appPath); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(7, 10)); + comps.Should().HaveLabels("foo"); + } + + [TestMethod, Priority(0)] + public async Task SysModuleChain() { + const string content1 = @"import module2.mod as mod +mod."; + const string content2 = @"import module3 as mod"; + const string content3 = @"import sys +sys.modules['module2.mod'] = None +VALUE = 42"; + + var uri1 = await TestData.CreateTestSpecificFileAsync("module1.py", content1); + var uri2 = await TestData.CreateTestSpecificFileAsync("module2.py", content2); + var uri3 = await TestData.CreateTestSpecificFileAsync("module3.py", content3); + + var root = TestData.GetTestSpecificRootUri().AbsolutePath; + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + var rdt = Services.GetService(); + + var doc1 = rdt.OpenDocument(uri1, content1); + rdt.OpenDocument(uri2, content2); + rdt.OpenDocument(uri3, content3); + + var analysis = await doc1.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 5)); + comps.Should().HaveLabels("VALUE"); + } + + [TestMethod, Priority(0)] + public async Task SysModuleChain_SingleOpen() { + const string content = @"import module1.mod as mod +mod."; + await TestData.CreateTestSpecificFileAsync("module1.py", @"import module2 as mod"); + await TestData.CreateTestSpecificFileAsync("module2.py", @"import sys +sys.modules['module1.mod'] = None +VALUE = 42"); + + var root = TestData.GetTestSpecificRootUri().AbsolutePath; + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + var rdt = Services.GetService(); + + var doc = rdt.OpenDocument(TestData.GetDefaultModuleUri(), content); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 5)); + comps.Should().HaveLabels("VALUE"); + } + + [TestMethod, Priority(0)] + public async Task UncSearchPaths() { + const string module1Path = @"q:\Folder\package\module1.py"; + const string module2Path = @"\\machine\share\package\module2.py"; + + const string appCode1 = @"from package import "; + const string appCode2 = @"from package import module1, module2 +module1. +module2."; + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + var rdt = Services.GetService(); + var interpreter = Services.GetService(); + interpreter.ModuleResolution.SetUserSearchPaths(new[] { @"q:\Folder\", @"\\machine\share\" }); + + rdt.OpenDocument(new Uri(module1Path), "X = 42"); + rdt.OpenDocument(new Uri(module2Path), "Y = 6 * 9"); + + var doc = rdt.OpenDocument(new Uri(appPath), appCode1); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(1, 21)); + comps.Should().HaveLabels("module1", "module2"); + + doc.Update(new[] { + new DocumentChange { + InsertedText = appCode2, + ReplacedSpan = new SourceSpan(1, 1, 1, 21) + } + }); + + analysis = await doc.GetAnalysisAsync(); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 9)); + comps.Should().HaveLabels("X").And.NotContainLabels("Y"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 9)); + comps.Should().HaveLabels("Y").And.NotContainLabels("X"); + } + + [TestMethod, Priority(0)] + public async Task UserSearchPathsInsideWorkspace() { + var folder1 = TestData.GetTestSpecificPath("folder1"); + var folder2 = TestData.GetTestSpecificPath("folder2"); + var packageInFolder1 = Path.Combine(folder1, "package"); + var packageInFolder2 = Path.Combine(folder2, "package"); + var module1Path = Path.Combine(packageInFolder1, "module1.py"); + var module2Path = Path.Combine(packageInFolder2, "module2.py"); + const string module1Content = @"class A(): + @staticmethod + def method1(): + pass"; + const string module2Content = @"class B(): + @staticmethod + def method2(): + pass"; + const string mainContent = @"from package import module1 as mod1, module2 as mod2 +mod1. +mod2. +mod1.A. +mod2.B."; + var root = Path.GetDirectoryName(folder1); + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + + var rdt = Services.GetService(); + var interpreter = Services.GetService(); + interpreter.ModuleResolution.SetUserSearchPaths(new[] { folder1, folder2 }); + + rdt.OpenDocument(new Uri(module1Path), module1Content); + rdt.OpenDocument(new Uri(module2Path), module2Content); + + var mainPath = Path.Combine(root, "main.py"); + var doc = rdt.OpenDocument(new Uri(mainPath), mainContent); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 6)); + comps.Should().HaveLabels("A").And.NotContainLabels("B"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(3, 6)); + comps.Should().HaveLabels("B").And.NotContainLabels("A"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(4, 8)); + comps.Should().HaveLabels("method1"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(5, 8)); + comps.Should().HaveLabels("method2"); + } + + [TestMethod, Priority(0)] + public async Task PackageModuleImport() { + const string appCode = @" +import package.sub_package.module1 +import package.sub_package.module2 + +package. +package.sub_package. +package.sub_package.module1. +package.sub_package.module2."; + + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + var rdt = Services.GetService(); + + var module1Path = Path.Combine(root, "package", "sub_package", "module1.py"); + var module2Path = Path.Combine(root, "package", "sub_package", "module2.py"); + + rdt.OpenDocument(new Uri(module1Path), "X = 42"); + rdt.OpenDocument(new Uri(module2Path), "Y = 6 * 9"); + + var doc = rdt.OpenDocument(new Uri(appPath), appCode); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(5, 9)); + comps.Should().OnlyHaveLabels("sub_package"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(6, 21)); + comps.Should().OnlyHaveLabels("module1", "module2"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(7, 29)); + comps.Should().HaveLabels("X").And.NotContainLabels("Y"); + + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(8, 29)); + comps.Should().HaveLabels("Y").And.NotContainLabels("X"); + } + + [TestMethod, Priority(0)] + public async Task TypingModule() { + var analysis = await GetAnalysisAsync(@"from typing import "); + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(1, 20)); + comps.Should().HaveLabels("TypeVar", "List", "Dict", "Union"); + } + + [DataRow(@"from package import sub_package; import package.sub_package.module1")] + [DataRow(@"import package.sub_package.module1; from package import sub_package")] + [DataRow(@"from package import sub_package; from package.sub_package import module")] + [DataRow(@"from package.sub_package import module; from package import sub_package")] + [Ignore("Not yet implemented")] + [TestMethod, Priority(0)] + public async Task FromImport_ModuleAffectsPackage(string appCodeImport) { + var appCode1 = appCodeImport + Environment.NewLine + "sub_package."; + var appCode2 = appCodeImport + Environment.NewLine + "sub_package.module."; + + var appPath = TestData.GetTestSpecificPath("app.py"); + var root = Path.GetDirectoryName(appPath); + await CreateServicesAsync(root, PythonVersions.LatestAvailable3X); + var rdt = Services.GetService(); + + var modulePath = Path.Combine(root, "package", "sub_package", "module.py"); + + rdt.OpenDocument(new Uri(modulePath), "X = 42"); + var doc = rdt.OpenDocument(new Uri(appPath), appCode1); + var analysis = await doc.GetAnalysisAsync(); + + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 13)); + comps.Should().OnlyHaveLabels("module"); + + doc.Update(new [] { + new DocumentChange { + InsertedText = appCode2, + ReplacedSpan = new SourceSpan(1, 1, 2, 13) + } + }); + + analysis = await doc.GetAnalysisAsync(); + comps = await cs.GetCompletionsAsync(analysis, new SourceLocation(2, 21)); + comps.Should().HaveLabels("X"); + } + } +}