Repository: elm-tooling/elm-language-server (ts)

Issue:
Show completions from both modules when multiple imports have the same name/alias
When there are multiple imports with the same name or alias, the completions should show from both modules. 

Pull request description:
Show completions for multiple modules if they share a name/alias
Closes #405. 

Patch:
diff --git a/package-lock.json b/package-lock.json
index a5a1dec..f3206b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -434,9 +434,9 @@
       }
     },
     "@elm-tooling/tree-sitter-elm": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/@elm-tooling/tree-sitter-elm/-/tree-sitter-elm-5.2.2.tgz",
-      "integrity": "sha512-FzEEttA7ujo69Z/HpuM5vvEDiCBL/Fbojak+y3jAJaYJaVWv7JnZUl2GdwbhIy25sNoLXFHtRbjK8wMi51y4aw==",
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/@elm-tooling/tree-sitter-elm/-/tree-sitter-elm-5.2.1.tgz",
+      "integrity": "sha512-aOFsGcoHdzOtO/yLEcj275Gx7VCM6yZezRA7kFyZbJX9icG6a+Q9LO02R/vZUrpSqSMXOAABFuJ2SCbzP6wCaQ==",
       "dev": true,
       "requires": {
         "nan": "^2.14.2",
diff --git a/package.json b/package.json
index d04ee1f..4694124 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
     "jest-mock-extended": "^1.0.13",
     "prettier": "^2.2.1",
     "tree-sitter-cli": "^0.18.2",
-    "@elm-tooling/tree-sitter-elm": "^5.2.2",
+    "@elm-tooling/tree-sitter-elm": "^5.2.1",
     "ts-jest": "^26.5.1",
     "ts-node": "^9.1.1",
     "typescript": "4.1.5"
diff --git a/src/providers/astProvider.ts b/src/providers/astProvider.ts
index d7d197d..d5dc247 100644
--- a/src/providers/astProvider.ts
+++ b/src/providers/astProvider.ts
@@ -143,7 +143,7 @@ export class ASTProvider {
     );
 
     oldNodes.forEach((node) => {
-      if (!newIds.has(node.id)) {
+      if (node && !newIds.has(node.id)) {
         params.program.getTypeCache().invalidateValueDeclaration(node);
         params.program.getTypeCache().invalidateTypeAnnotation(node);
         params.program.getTypeCache().invalidateTypeOrTypeAlias(node);
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index ff7debd..2d39b91 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -28,6 +28,7 @@ import { TypeChecker } from "../compiler/typeChecker";
 import escapeStringRegexp from "escape-string-regexp";
 import { TRecord } from "../compiler/typeInference";
 import { ICompletionParams } from "./paramsExtensions";
+import { Utils } from "../util/utils";
 
 export type CompletionResult =
   | CompletionItem[]
@@ -1236,31 +1237,49 @@ export class CompletionProvider {
       return result;
     }
 
-    let alreadyImported = true;
+    let alreadyImported = false;
 
-    // Try to find the module definition that is already imported
-    const definitionNode = checker.findDefinition(node, sourceFile).symbol;
+    const matchedSourceFiles: ISourceFile[] = [];
 
-    let moduleTree: ISourceFile | undefined;
+    const imports =
+      sourceFile.symbolLinks
+        ?.get(sourceFile.tree.rootNode)
+        ?.getAll(targetModule)
+        ?.filter((symbol) => symbol.type === "Import") ?? [];
 
-    if (
-      definitionNode?.node &&
-      TreeUtils.findParentOfType("module_declaration", definitionNode.node)
-    ) {
-      moduleTree = program.getSourceFile(definitionNode.node.tree.uri);
-    } else {
-      // Try to find this module in the forest to import
-      moduleTree = program.getSourceFileOfImportableModule(
+    if (imports.length > 0) {
+      matchedSourceFiles.push(
+        ...imports
+          .map((imp) => {
+            const moduleName =
+              imp.node.childForFieldName("moduleName")?.text ?? "";
+
+            return program.getSourceFileOfImportableModule(
+              sourceFile,
+              moduleName,
+            );
+          })
+          .filter(Utils.notUndefined.bind(this)),
+      );
+
+      alreadyImported = true;
+    } else if (!checker.getAllImports(sourceFile).getModule(targetModule)) {
+      // Try to find a module that may not be imported
+      const moduleSourceFile = program.getSourceFileOfImportableModule(
         sourceFile,
         targetModule,
       );
-      alreadyImported = false;
+
+      if (moduleSourceFile) {
+        matchedSourceFiles.push(moduleSourceFile);
+        alreadyImported = false;
+      }
     }
 
-    if (moduleTree) {
-      // Get exposed values
-      const imports = ImportUtils.getPossibleImportsOfTree(moduleTree);
-      imports.forEach((value) => {
+    // Get exposed values
+    matchedSourceFiles
+      .flatMap(ImportUtils.getPossibleImportsOfTree.bind(this))
+      .forEach((value) => {
         const markdownDocumentation = HintHelper.createHint(value.node);
         let additionalTextEdits: TextEdit[] | undefined;
         let detail: string | undefined;
@@ -1301,7 +1320,6 @@ export class CompletionProvider {
             break;
         }
       });
-    }
 
     return result;
   }


Test patch:
diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts
index 79e9206..b942f4f 100644
--- a/test/completionProvider.test.ts
+++ b/test/completionProvider.test.ts
@@ -1503,4 +1503,83 @@ func model =
 
     await testCompletions(source, ["details"]);
   });
+
+  it("Completions for module alias if using an import alias", async () => {
+    const source = `
+--@ Module.elm
+module Module exposing (..)
+
+foo = ""
+
+type Maybe a = Just a | Nothing
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module as M
+
+func =
+    M.f{-caret-}
+`;
+
+    await testCompletions(
+      source,
+      ["foo", "Maybe", "Just", "Nothing"],
+      "exactMatch",
+    );
+  });
+
+  it("No completions for module if using an import alias", async () => {
+    const source = `
+--@ Module.elm
+module Module exposing (..)
+
+foo = ""
+
+type Maybe a = Just a | Nothing
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module as M
+
+func =
+    Module.f{-caret-}
+`;
+
+    await testCompletions(source, [], "exactMatch");
+  });
+
+  it("Multiple modules with the same name/alias should have all completions", async () => {
+    const source = `
+--@ Module.elm
+module Module exposing (..)
+
+foo = ""
+
+type Maybe a = Just a | Nothing
+
+--@ OtherModule.elm
+module OtherModule exposing (..)
+
+bar = ""
+
+type Result e a = Ok a | Err e
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module as M
+import OtherModule as M
+
+func =
+    M.f{-caret-}
+`;
+
+    await testCompletions(
+      source,
+      ["foo", "Maybe", "Just", "Nothing", "bar", "Result", "Ok", "Err"],
+      "exactMatch",
+    );
+  });
 });