Repository: elm-tooling/elm-language-server
Pull request #298 (2020-07-03 20:21:57)

Add value completions for non-imported modules
Trying to access a non-imported module like `Module.Sub.{-caret-}` will now show those possible values as completions and will auto import `Module.Sub` on completion. Previously this only worked on already imported modules.


Patch:
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 89a8610..da82d72 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -13,7 +13,7 @@ import {
 import { URI } from "vscode-uri";
 import { SyntaxNode, Tree } from "web-tree-sitter";
 import { IElmWorkspace } from "../elmWorkspace";
-import { IForest } from "../forest";
+import { IForest, ITreeContainer } from "../forest";
 import { IImports } from "../imports";
 import { getEmptyTypes } from "../util/elmUtils";
 import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher";
@@ -1062,6 +1062,9 @@ export class CompletionProvider {
     if (moduleNodes && moduleNodes.length > 0) {
       const moduleNode = moduleNodes[moduleNodes.length - 1];
 
+      let alreadyImported = true;
+
+      // Try to find the module definition that is already imported
       const definitionNode = TreeUtils.findDefinitionNodeByReferencingNode(
         moduleNode,
         uri,
@@ -1069,38 +1072,60 @@ export class CompletionProvider {
         imports,
       );
 
+      let moduleTree: ITreeContainer | undefined;
+
       if (definitionNode && definitionNode.nodeType === "Module") {
+        moduleTree = forest.getByUri(definitionNode.uri);
+      } else {
+        // Try to find this module in the forest to import
+        moduleTree = forest.getByModuleName(targetModule);
+        alreadyImported = false;
+      }
+
+      if (moduleTree) {
         // Get exposed values
-        const tree = forest.getByUri(definitionNode.uri);
-
-        if (tree) {
-          const imports = ImportUtils.getPossibleImportsOfTree(tree);
-          imports.forEach((value) => {
-            const markdownDocumentation = HintHelper.createHint(value.node);
-            const completionOptions = {
-              label: value.value,
-              sortPrefix: "a",
-              range,
-              markdownDocumentation,
-            };
-            switch (value.type) {
-              case "Function":
-                result.push(this.createFunctionCompletion(completionOptions));
-                break;
-              case "Type":
-                result.push(this.createTypeCompletion(completionOptions));
-                break;
-              case "TypeAlias":
-                result.push(this.createTypeAliasCompletion(completionOptions));
-                break;
-              case "UnionConstructor":
-                result.push(
-                  this.createUnionConstructorCompletion(completionOptions),
-                );
-                break;
+        const imports = ImportUtils.getPossibleImportsOfTree(moduleTree);
+        imports.forEach((value) => {
+          const markdownDocumentation = HintHelper.createHint(value.node);
+          let additionalTextEdits: TextEdit[] | undefined;
+          let detail: string | undefined;
+
+          // Add the import text edit if not imported
+          if (!alreadyImported) {
+            const importEdit = RefactorEditUtils.addImport(tree, targetModule);
+
+            if (importEdit) {
+              additionalTextEdits = [importEdit];
+              detail = `Auto import module '${targetModule}'`;
             }
-          });
-        }
+          }
+
+          const completionOptions: ICompletionOptions = {
+            label: value.value,
+            sortPrefix: "a",
+            range,
+            markdownDocumentation,
+            additionalTextEdits,
+            detail,
+          };
+
+          switch (value.type) {
+            case "Function":
+              result.push(this.createFunctionCompletion(completionOptions));
+              break;
+            case "Type":
+              result.push(this.createTypeCompletion(completionOptions));
+              break;
+            case "TypeAlias":
+              result.push(this.createTypeAliasCompletion(completionOptions));
+              break;
+            case "UnionConstructor":
+              result.push(
+                this.createUnionConstructorCompletion(completionOptions),
+              );
+              break;
+          }
+        });
       }
     }
 


Test patch:
diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts
index 487b5bb..5825c90 100644
--- a/test/completionProvider.test.ts
+++ b/test/completionProvider.test.ts
@@ -842,7 +842,7 @@ test = Module.Submodule.{-caret-}
 
     await testCompletions(
       source2,
-      ["AnotherSubmodule"],
+      ["AnotherSubmodule", "func"],
       "exactMatch",
       "triggeredByDot",
     );
@@ -947,6 +947,67 @@ test = Module.{-caret-}
     );
   });
 
+  it("Non imported qualified modules should have value completions with auto imports", async () => {
+    const source = `
+--@ Module.elm
+module Module exposing (..)
+
+testFunc = ""
+
+type Msg = Msg1 | Msg2
+
+type alias Model = { field : String }
+
+--@ Test.elm
+module Test exposing (..)
+
+test = div [] [ Module.{-caret-} ]
+`;
+
+    await testCompletions(
+      source,
+      [
+        {
+          label: "testFunc",
+          detail: "Auto import module 'Module'",
+          additionalTextEdits: [
+            TextEdit.insert(Position.create(1, 0), "import Module\n"),
+          ],
+        },
+        {
+          label: "Msg",
+          detail: "Auto import module 'Module'",
+          additionalTextEdits: [
+            TextEdit.insert(Position.create(1, 0), "import Module\n"),
+          ],
+        },
+        {
+          label: "Msg1",
+          detail: "Auto import module 'Module'",
+          additionalTextEdits: [
+            TextEdit.insert(Position.create(1, 0), "import Module\n"),
+          ],
+        },
+        {
+          label: "Msg2",
+          detail: "Auto import module 'Module'",
+          additionalTextEdits: [
+            TextEdit.insert(Position.create(1, 0), "import Module\n"),
+          ],
+        },
+        {
+          label: "Model",
+          detail: "Auto import module 'Module'",
+          additionalTextEdits: [
+            TextEdit.insert(Position.create(1, 0), "import Module\n"),
+          ],
+        },
+      ],
+      "exactMatch",
+      "triggeredByDot",
+    );
+  });
+
   // Ref: https://github.com/elm-tooling/elm-language-server/issues/288
   it("Record completions should not interfere with Module completions", async () => {
     const source = `