Repository: elm-tooling/elm-language-server
Pull request #291 (2020-06-17 06:39:38)

Completion improvements
## Summary of changes:
### Improved sorting of qualified imports and auto imports. Ref #287.
The problem is that if you have `Html` imported and you type `div`, there are 2 completions: `div (auto imports)` and `Html.div`. The auto imported will always be first because it matches the string better (in VSCode at least). To fix this, I check if the typed text matches the function name and add filter text to `Html.div` completion. So if we type `di`, `Html.div` gets a filter text of `div`, and thus will sort to the top. If we start to type `Ht`, the filter text will just be the label, `Html.div`. This is the main fix for #287, but there are still some cases I want to look at later.

### Added completions for module values or possible submodules
If a module is imported, it now should all possible values for that module. Ex: `Html.{-caret-}` will show `a, div, span, ...`. It also shows possible submodules (not imported). In the case: `Attributes` or `Styled.Attributes` could also show up as completions. 
 
### Fixed record completion interfering with module completions. Ref #288.   
Reverts #289 with a proper fix. Instead or checking for all `field_access_segment` descendents, it now specifically looks at the target word. 

I also refactored `CompletionProvider` to use object parameters instead of function parameters. It made it more clear and easier to pass in optional parameters (like `filterText`).

And of course, added a lot of tests to cover these cases. 


Patch:
diff --git a/src/elmWorkspace.ts b/src/elmWorkspace.ts
index 63b85af..234b426 100644
--- a/src/elmWorkspace.ts
+++ b/src/elmWorkspace.ts
@@ -307,7 +307,7 @@ export class ElmWorkspace implements IElmWorkspace {
             URI.file(
               pathToPackage
                 .concat("/src/")
-                .concat(element.replace(".", "/").concat(".elm")),
+                .concat(element.split(".").join("/").concat(".elm")),
             ),
           );
         } else {
@@ -316,7 +316,7 @@ export class ElmWorkspace implements IElmWorkspace {
               URI.file(
                 pathToPackage
                   .concat("/src/")
-                  .concat(element.replace(".", "/").concat(".elm")),
+                  .concat(element.split(".").join("/").concat(".elm")),
               ),
             ),
           );
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 264ba77..584a603 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -29,6 +29,17 @@ export type CompletionResult =
   | null
   | undefined;
 
+interface ICompletionOptions {
+  label: string;
+  range: Range;
+  sortPrefix: string;
+  kind?: CompletionItemKind;
+  markdownDocumentation?: string | undefined;
+  detail?: string;
+  additionalTextEdits?: TextEdit[];
+  filterText?: string;
+}
+
 export class CompletionProvider {
   private qidRegex = /[a-zA-Z0-9.]+/;
   private qidAtStartOfLineRegex = /^[a-zA-Z0-9 .]*$/;
@@ -88,6 +99,11 @@ export class CompletionProvider {
         targetLine.slice(0, params.position.character - 1),
       );
 
+      const targetWord = targetLine.substring(
+        replaceRange.start.character,
+        replaceRange.end.character,
+      );
+
       if (
         isAtStartOfLine &&
         nodeAtLineBefore.type === "lower_case_identifier" &&
@@ -95,13 +111,12 @@ export class CompletionProvider {
         nodeAtLineBefore.parent.type === "type_annotation"
       ) {
         return [
-          this.createCompletion(
-            undefined,
-            CompletionItemKind.Text,
-            nodeAtLineBefore.text,
-            replaceRange,
-            "a",
-          ),
+          this.createCompletion({
+            kind: CompletionItemKind.Text,
+            label: nodeAtLineBefore.text,
+            range: replaceRange,
+            sortPrefix: "a",
+          }),
         ];
       } else if (
         isAtStartOfLine &&
@@ -111,13 +126,12 @@ export class CompletionProvider {
           nodeAtLineAfter.parent.type === "function_declaration_left")
       ) {
         return [
-          this.createCompletion(
-            undefined,
-            CompletionItemKind.Text,
-            nodeAtLineAfter.text,
-            replaceRange,
-            "a",
-          ),
+          this.createCompletion({
+            kind: CompletionItemKind.Text,
+            label: nodeAtLineAfter.text,
+            range: replaceRange,
+            sortPrefix: "a",
+          }),
         ];
       } else if (previousWord && previousWord === "module") {
         return undefined;
@@ -174,30 +188,42 @@ export class CompletionProvider {
           forest,
         );
       } else if (
-        (nodeAtPosition.type === "field_access_segment" ||
-          nodeAtPosition.parent?.type === "field_access_segment" ||
-          TreeUtils.descendantsOfType(nodeAtPosition, "field_access_segment")
-            .length > 0) &&
-        nodeAtPosition.parent
+        nodeAtPosition.type === "field_access_segment" ||
+        nodeAtPosition.parent?.type === "field_access_segment" ||
+        RegExp("^[a-z]+.*.$").exec(targetWord)
       ) {
-        const accessSegmentNode =
+        let accessSegmentNode =
           nodeAtPosition.type === "field_access_segment"
             ? nodeAtPosition
             : nodeAtPosition.parent?.type === "field_access_segment"
             ? nodeAtPosition.parent
-            : TreeUtils.descendantsOfType(
-                nodeAtPosition,
-                "field_access_segment",
-              )[0];
+            : undefined;
+
+        if (!accessSegmentNode) {
+          accessSegmentNode =
+            TreeUtils.getNamedDescendantForPosition(tree.rootNode, {
+              ...params.position,
+              character: params.position.character - 1,
+            }).parent ?? undefined;
+        }
 
-        const dotIndex = TreeUtils.findFirstNamedChildOfType(
-          "dot",
-          accessSegmentNode,
-        )?.startPosition.column;
+        if (accessSegmentNode) {
+          let dotIndex = TreeUtils.findFirstNamedChildOfType(
+            "dot",
+            accessSegmentNode,
+          )?.startPosition.column;
 
-        if (dotIndex) {
-          completions.push(
-            ...this.getRecordCompletions(
+          if (!dotIndex) {
+            const wordDot = targetWord.lastIndexOf(".");
+
+            if (wordDot >= 0) {
+              dotIndex =
+                params.position.character - (targetWord.length - wordDot);
+            }
+          }
+
+          if (dotIndex) {
+            return this.getRecordCompletions(
               accessSegmentNode,
               tree,
               Range.create(
@@ -207,8 +233,51 @@ export class CompletionProvider {
               elmWorkspace.getImports(),
               params.textDocument.uri,
               forest,
-            ),
+            );
+          }
+        }
+      } else if (
+        (nodeAtPosition.parent?.type === "value_qid" &&
+          nodeAtPosition.previousNamedSibling?.type === "dot") ||
+        RegExp("^[A-Z]+.*.$").exec(targetWord) // Handles the 'Html.Attributes.' case
+      ) {
+        let targetNode = nodeAtPosition.parent;
+
+        if (targetNode?.type !== "value_qid") {
+          targetNode = TreeUtils.getNamedDescendantForPosition(tree.rootNode, {
+            ...params.position,
+            character: params.position.character - 1,
+          }).parent;
+        }
+
+        if (targetNode) {
+          const dotNodes = TreeUtils.findAllNamedChildrenOfType(
+            "dot",
+            targetNode,
           );
+
+          // Get the last dot index
+          const dotIndex =
+            dotNodes && dotNodes.length > 0
+              ? dotNodes[dotNodes.length - 1].startPosition.column
+              : targetWord.endsWith(".")
+              ? params.position.character - 1
+              : undefined;
+
+          if (dotIndex) {
+            return this.getSubmodulesOrValues(
+              targetNode,
+              params.textDocument.uri,
+              tree,
+              elmWorkspace.getImports(),
+              forest,
+              Range.create(
+                Position.create(params.position.line, dotIndex + 1),
+                params.position,
+              ),
+              targetLine.substring(replaceRange.start.character, dotIndex),
+            );
+          }
         }
       }
 
@@ -219,11 +288,17 @@ export class CompletionProvider {
         ...this.findDefinitionsForScope(nodeAtPosition, tree, replaceRange),
       );
 
+      const currentTarget = targetLine.substring(
+        replaceRange.start.character,
+        replaceRange.end.character,
+      );
+
       completions.push(
         ...this.getCompletionsFromOtherFile(
           elmWorkspace.getImports(),
           params.textDocument.uri,
           replaceRange,
+          currentTarget,
         ),
       );
 
@@ -265,7 +340,13 @@ export class CompletionProvider {
   ): CompletionItem[] {
     return forest.treeIndex
       .filter((a) => a.moduleName)
-      .map((a) => this.createModuleCompletion(a.moduleName!, range, "b"));
+      .map((a) =>
+        this.createModuleCompletion({
+          label: a.moduleName!,
+          range,
+          sortPrefix: "b",
+        }),
+      );
   }
 
   private getExposedFromModule(
@@ -296,23 +377,45 @@ export class CompletionProvider {
             switch (a.type) {
               case "TypeAlias":
                 return [
-                  this.createTypeAliasCompletion(value, a.name, range, prefix),
+                  this.createTypeAliasCompletion({
+                    markdownDocumentation: value,
+                    label: a.name,
+                    range,
+                    sortPrefix: prefix,
+                  }),
                 ];
               case "Type":
                 return a.exposedUnionConstructors
                   ? [
-                      this.createTypeCompletion(
-                        value,
-                        `${a.name}(..)`,
+                      this.createTypeCompletion({
+                        markdownDocumentation: value,
+                        label: `${a.name}(..)`,
                         range,
-                        prefix,
-                      ),
-                      this.createTypeCompletion(value, a.name, range, prefix),
+                        sortPrefix: prefix,
+                      }),
+                      this.createTypeCompletion({
+                        markdownDocumentation: value,
+                        label: a.name,
+                        range,
+                        sortPrefix: prefix,
+                      }),
                     ]
-                  : [this.createTypeCompletion(value, a.name, range, prefix)];
+                  : [
+                      this.createTypeCompletion({
+                        markdownDocumentation: value,
+                        label: a.name,
+                        range,
+                        sortPrefix: prefix,
+                      }),
+                    ];
               default:
                 return [
-                  this.createFunctionCompletion(value, a.name, range, prefix),
+                  this.createFunctionCompletion({
+                    markdownDocumentation: value,
+                    label: a.name,
+                    range,
+                    sortPrefix: prefix,
+                  }),
                 ];
             }
           })
@@ -325,6 +428,7 @@ export class CompletionProvider {
     imports: IImports,
     uri: string,
     range: Range,
+    inputText: string,
   ): CompletionItem[] {
     const completions: CompletionItem[] = [];
 
@@ -343,49 +447,69 @@ export class CompletionProvider {
           }
         }
 
+        const label = element.alias;
+        let filterText = label;
+
+        const dotIndex = label.lastIndexOf(".");
+        const valuePart = label.slice(dotIndex + 1);
+
+        // Try to determine if just the value is being typed
+        if (valuePart.toLowerCase().startsWith(inputText.toLowerCase())) {
+          filterText = valuePart;
+        }
+
         switch (element.type) {
           case "Function":
             completions.push(
-              this.createFunctionCompletion(
-                value,
-                element.alias,
+              this.createFunctionCompletion({
+                markdownDocumentation: value,
+                label,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+                filterText,
+              }),
             );
             break;
           case "UnionConstructor":
             completions.push(
-              this.createUnionConstructorCompletion(
-                element.alias,
+              this.createUnionConstructorCompletion({
+                label,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+                filterText,
+              }),
             );
             break;
           case "Operator":
             completions.push(
-              this.createOperatorCompletion(
-                value,
-                element.alias,
+              this.createOperatorCompletion({
+                markdownDocumentation: value,
+                label,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+              }),
             );
             break;
           case "Type":
             completions.push(
-              this.createTypeCompletion(value, element.alias, range, prefix),
+              this.createTypeCompletion({
+                markdownDocumentation: value,
+                label,
+                range,
+                sortPrefix: prefix,
+                filterText,
+              }),
             );
             break;
           case "TypeAlias":
             completions.push(
-              this.createTypeAliasCompletion(
-                value,
-                element.alias,
+              this.createTypeAliasCompletion({
+                markdownDocumentation: value,
+                label,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+                filterText,
+              }),
             );
             break;
           // Do not handle operators, they are not valid if prefixed
@@ -395,7 +519,13 @@ export class CompletionProvider {
 
     completions.push(
       ...getEmptyTypes().map((a) =>
-        this.createCompletion(a.markdown, a.symbolKind, a.name, range, "d0000"),
+        this.createCompletion({
+          markdownDocumentation: a.markdown,
+          kind: a.symbolKind,
+          label: a.name,
+          range,
+          sortPrefix: "d0000",
+        }),
       ),
     );
 
@@ -424,12 +554,12 @@ export class CompletionProvider {
       for (const declaration of declarations) {
         const value = HintHelper.createHint(declaration);
         completions.push(
-          this.createFunctionCompletion(
-            value,
-            declaration.firstNamedChild!.firstNamedChild!.text,
+          this.createFunctionCompletion({
+            markdownDocumentation: value,
+            label: declaration.firstNamedChild!.firstNamedChild!.text,
             range,
-            prefix,
-          ),
+            sortPrefix: prefix,
+          }),
         );
       }
     }
@@ -444,16 +574,21 @@ export class CompletionProvider {
         );
         if (name) {
           completions.push(
-            this.createTypeCompletion(value, name.text, range, prefix),
+            this.createTypeCompletion({
+              markdownDocumentation: value,
+              label: name.text,
+              range,
+              sortPrefix: prefix,
+            }),
           );
           if (moduleDefinition) {
             completions.push(
-              this.createTypeCompletion(
-                value,
-                `${name.text}(..)`,
+              this.createTypeCompletion({
+                markdownDocumentation: value,
+                label: `${name.text}(..)`,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+              }),
             );
           }
         }
@@ -469,11 +604,11 @@ export class CompletionProvider {
           );
           if (unionVariantName) {
             completions.push(
-              this.createUnionConstructorCompletion(
-                unionVariantName.text,
+              this.createUnionConstructorCompletion({
+                label: unionVariantName.text,
                 range,
-                prefix,
-              ),
+                sortPrefix: prefix,
+              }),
             );
           }
         }
@@ -490,7 +625,12 @@ export class CompletionProvider {
         );
         if (name) {
           completions.push(
-            this.createTypeAliasCompletion(value, name.text, range, prefix),
+            this.createTypeAliasCompletion({
+              markdownDocumentation: value,
+              label: name.text,
+              range,
+              sortPrefix: prefix,
+            }),
           );
         }
       }
@@ -560,22 +700,10 @@ export class CompletionProvider {
   }
 
   private createFunctionCompletion(
-    markdownDocumentation: string | undefined,
-    label: string,
-    range: Range,
-    sortPrefix: string,
-    detail?: string,
-    addImportTextEdit?: TextEdit,
+    options: ICompletionOptions,
   ): CompletionItem {
-    return this.createCompletion(
-      markdownDocumentation,
-      CompletionItemKind.Function,
-      label,
-      range,
-      sortPrefix,
-      detail,
-      addImportTextEdit ? [addImportTextEdit] : undefined,
-    );
+    options.kind = CompletionItemKind.Function;
+    return this.createCompletion(options);
   }
 
   private createFieldOrParameterCompletion(
@@ -591,113 +719,52 @@ export class CompletionProvider {
     );
   }
 
-  private createTypeCompletion(
-    markdownDocumentation: string | undefined,
-    label: string,
-    range: Range,
-    sortPrefix: string,
-    detail?: string,
-    addImportTextEdit?: TextEdit,
-  ): CompletionItem {
-    return this.createCompletion(
-      markdownDocumentation,
-      CompletionItemKind.Enum,
-      label,
-      range,
-      sortPrefix,
-      detail,
-      addImportTextEdit ? [addImportTextEdit] : undefined,
-    );
+  private createTypeCompletion(options: ICompletionOptions): CompletionItem {
+    options.kind = CompletionItemKind.Enum;
+    return this.createCompletion(options);
   }
 
   private createTypeAliasCompletion(
-    markdownDocumentation: string | undefined,
-    label: string,
-    range: Range,
-    sortPrefix: string,
-    detail?: string,
-    addImportTextEdit?: TextEdit,
+    options: ICompletionOptions,
   ): CompletionItem {
-    return this.createCompletion(
-      markdownDocumentation,
-      CompletionItemKind.Struct,
-      label,
-      range,
-      sortPrefix,
-      detail,
-      addImportTextEdit ? [addImportTextEdit] : undefined,
-    );
+    options.kind = CompletionItemKind.Struct;
+    return this.createCompletion(options);
   }
 
   private createOperatorCompletion(
-    markdownDocumentation: string | undefined,
-    label: string,
-    range: Range,
-    sortPrefix: string,
+    options: ICompletionOptions,
   ): CompletionItem {
-    return this.createCompletion(
-      markdownDocumentation,
-      CompletionItemKind.Operator,
-      label,
-      range,
-      sortPrefix,
-    );
+    options.kind = CompletionItemKind.Operator;
+    return this.createCompletion(options);
   }
 
   private createUnionConstructorCompletion(
-    label: string,
-    range: Range,
-    sortPrefix: string,
-    detail?: string,
-    addImportTextEdit?: TextEdit,
+    options: ICompletionOptions,
   ): CompletionItem {
-    return this.createCompletion(
-      undefined,
-      CompletionItemKind.EnumMember,
-      label,
-      range,
-      sortPrefix,
-      detail,
-      addImportTextEdit ? [addImportTextEdit] : undefined,
-    );
+    options.kind = CompletionItemKind.EnumMember;
+    return this.createCompletion(options);
   }
 
-  private createModuleCompletion(
-    label: string,
-    range: Range,
-    sortPrefix: string,
-  ): CompletionItem {
-    return this.createCompletion(
-      undefined,
-      CompletionItemKind.Module,
-      label,
-      range,
-      sortPrefix,
-    );
+  private createModuleCompletion(options: ICompletionOptions): CompletionItem {
+    options.kind = CompletionItemKind.Module;
+    return this.createCompletion(options);
   }
 
-  private createCompletion(
-    markdownDocumentation: string | undefined,
-    kind: CompletionItemKind,
-    label: string,
-    range: Range,
-    sortPrefix: string,
-    detail?: string,
-    additionalTextEdits?: TextEdit[],
-  ): CompletionItem {
+  private createCompletion(options: ICompletionOptions): CompletionItem {
     return {
-      documentation: markdownDocumentation
+      documentation: options.markdownDocumentation
         ? {
             kind: MarkupKind.Markdown,
-            value: markdownDocumentation ?? "",
+            value: options.markdownDocumentation ?? "",
           }
         : undefined,
-      kind,
-      label,
-      sortText: `${sortPrefix}_${label}`,
-      textEdit: TextEdit.replace(range, label),
-      detail,
-      additionalTextEdits,
+      kind: options.kind,
+      label: options.label,
+      sortText: `${options.sortPrefix}_${options.label}`,
+      textEdit: TextEdit.replace(options.range, options.label),
+      detail: options.detail,
+      additionalTextEdits: options.additionalTextEdits,
+      filterText: options.filterText,
     };
   }
 
@@ -747,12 +814,12 @@ export class CompletionProvider {
                 nodeToProcess,
               );
               result.push(
-                this.createFunctionCompletion(
-                  value,
-                  nodeToProcess.firstNamedChild.firstNamedChild.text,
+                this.createFunctionCompletion({
+                  markdownDocumentation: value,
+                  label: nodeToProcess.firstNamedChild.firstNamedChild.text,
                   range,
-                  prefix,
-                ),
+                  sortPrefix: prefix,
+                }),
               );
             }
           });
@@ -774,7 +841,12 @@ export class CompletionProvider {
           caseBranchVariableNodes.forEach((a) => {
             const value = HintHelper.createHintFromDefinitionInCaseBranch();
             result.push(
-              this.createFunctionCompletion(value, a.text, range, prefix),
+              this.createFunctionCompletion({
+                markdownDocumentation: value,
+                label: a.text,
+                range,
+                sortPrefix: prefix,
+              }),
             );
           });
         }
@@ -853,7 +925,7 @@ export class CompletionProvider {
     const isIncomplete = possibleImports.length > 50;
 
     possibleImports.splice(0, 49).forEach((possibleImport, i) => {
-      const documentation = HintHelper.createHint(possibleImport.node);
+      const markdownDocumentation = HintHelper.createHint(possibleImport.node);
       const detail = `Auto import from module '${possibleImport.module}'`;
       const importTextEdit = RefactorEditUtils.addImport(
         tree,
@@ -861,48 +933,29 @@ export class CompletionProvider {
         possibleImport.valueToImport ?? possibleImport.value,
       );
 
+      const completionOptions = {
+        markdownDocumentation,
+        label: possibleImport.value,
+        range,
+        sortPrefix: `f${i}`,
+        detail,
+        additionalTextEdits: importTextEdit ? [importTextEdit] : undefined,
+      };
       if (possibleImport.type === "Function") {
-        result.push(
-          this.createFunctionCompletion(
-            documentation,
-            possibleImport.value,
-            range,
-            `e${i}`,
-            detail,
-            importTextEdit,
-          ),
-        );
+        result.push(this.createFunctionCompletion(completionOptions));
       } else if (possibleImport.type === "TypeAlias") {
-        result.push(
-          this.createTypeAliasCompletion(
-            documentation,
-            possibleImport.value,
-            range,
-            `e${i}`,
-            detail,
-            importTextEdit,
-          ),
-        );
+        result.push(this.createTypeAliasCompletion(completionOptions));
       } else if (possibleImport.type === "Type") {
-        result.push(
-          this.createTypeCompletion(
-            documentation,
-            possibleImport.value,
-            range,
-            `e${i}`,
-            detail,
-            importTextEdit,
-          ),
-        );
+        result.push(this.createTypeCompletion(completionOptions));
       } else if (possibleImport.type === "UnionConstructor") {
         result.push(
-          this.createUnionConstructorCompletion(
-            possibleImport.value,
+          this.createUnionConstructorCompletion({
+            label: possibleImport.value,
             range,
-            `e${i}`,
+            sortPrefix: `f${i}`,
             detail,
-            importTextEdit,
-          ),
+            additionalTextEdits: importTextEdit ? [importTextEdit] : undefined,
+          }),
         );
       }
     });
@@ -910,6 +963,113 @@ export class CompletionProvider {
     return { list: result, isIncomplete };
   }
 
+  private getSubmodulesOrValues(
+    node: SyntaxNode,
+    uri: string,
+    tree: Tree,
+    imports: IImports,
+    forest: IForest,
+    range: Range,
+    targetModule: string,
+  ): CompletionItem[] {
+    const result: CompletionItem[] = [];
+
+    // Handle possible submodules
+    forest.treeIndex
+      .filter(
+        (t) =>
+          t.moduleName?.startsWith(targetModule + ".") &&
+          t.moduleName !== targetModule,
+      )
+      .forEach((t) => {
+        const moduleNode = TreeUtils.findModuleDeclaration(t.tree);
+        if (t.moduleName) {
+          const markdownDocumentation = HintHelper.createHint(moduleNode);
+
+          result.push(
+            this.createModuleCompletion({
+              label: t.moduleName.slice(targetModule.length + 1),
+              sortPrefix: "b",
+              range,
+              markdownDocumentation,
+            }),
+          );
+        }
+      });
+
+    const moduleNodes = TreeUtils.findAllNamedChildrenOfType(
+      "upper_case_identifier",
+      node,
+    )?.filter((node) => node.text !== "");
+
+    if (moduleNodes && moduleNodes.length > 0) {
+      const moduleNode = moduleNodes[moduleNodes.length - 1];
+
+      const definitionNode = TreeUtils.findDefinitionNodeByReferencingNode(
+        moduleNode,
+        uri,
+        tree,
+        imports,
+      );
+
+      if (definitionNode && definitionNode.nodeType === "Module") {
+        // 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);
+            switch (value.type) {
+              case "Function":
+                result.push(
+                  this.createFunctionCompletion({
+                    label: value.value,
+                    sortPrefix: "a",
+                    range,
+                    markdownDocumentation,
+                  }),
+                );
+                break;
+              case "Type":
+                result.push(
+                  this.createTypeCompletion({
+                    label: value.value,
+                    sortPrefix: "a",
+                    range,
+                    markdownDocumentation,
+                  }),
+                );
+                break;
+              case "TypeAlias":
+                result.push(
+                  this.createTypeAliasCompletion({
+                    label: value.value,
+                    sortPrefix: "a",
+                    range,
+                    markdownDocumentation,
+                  }),
+                );
+                break;
+              case "UnionConstructor":
+                result.push(
+                  this.createUnionConstructorCompletion({
+                    label: value.value,
+                    sortPrefix: "a",
+                    range,
+                    markdownDocumentation,
+                  }),
+                );
+                break;
+            }
+          });
+        }
+      }
+    }
+
+    return result;
+  }
+
   private createSnippet(
     label: string,
     snippetText: string | string[],
diff --git a/src/util/importUtils.ts b/src/util/importUtils.ts
index 10834b9..df3970d 100644
--- a/src/util/importUtils.ts
+++ b/src/util/importUtils.ts
@@ -1,4 +1,4 @@
-import { IForest } from "../forest";
+import { IForest, ITreeContainer } from "../forest";
 import RANKING_LIST from "../providers/ranking";
 import { TreeUtils, NodeType } from "./treeUtils";
 import { SyntaxNode } from "web-tree-sitter";
@@ -89,38 +89,7 @@ export class ImportUtils {
             tree.uri !== uri,
         )
         .forEach((tree) => {
-          tree.exposing?.forEach((exposed) => {
-            const module = tree.moduleName;
-            if (module) {
-              exposedValues.push({
-                module,
-                value: exposed.name,
-                package: tree.maintainerAndPackageName,
-                type: exposed.type,
-                node: exposed.syntaxNode,
-              });
-
-              exposed.exposedUnionConstructors?.forEach((exp) => {
-                if (exp.syntaxNode.parent) {
-                  const value = TreeUtils.findFirstNamedChildOfType(
-                    "upper_case_identifier",
-                    exp.syntaxNode.parent,
-                  )?.text;
-
-                  if (value) {
-                    exposedValues.push({
-                      module,
-                      value: exp.name,
-                      valueToImport: `${value}(..)`,
-                      package: tree.maintainerAndPackageName,
-                      type: "UnionConstructor",
-                      node: exp.syntaxNode,
-                    });
-                  }
-                }
-              });
-            }
-          });
+          exposedValues.push(...ImportUtils.getPossibleImportsOfTree(tree));
         });
     }
 
@@ -168,6 +137,47 @@ export class ImportUtils {
     return exposedValues;
   }
 
+  public static getPossibleImportsOfTree(
+    tree: ITreeContainer,
+  ): IPossibleImport[] {
+    const exposedValues: IPossibleImport[] = [];
+
+    tree.exposing?.forEach((exposed) => {
+      const module = tree.moduleName;
+      if (module) {
+        exposedValues.push({
+          module,
+          value: exposed.name,
+          package: tree.maintainerAndPackageName,
+          type: exposed.type,
+          node: exposed.syntaxNode,
+        });
+
+        exposed.exposedUnionConstructors?.forEach((exp) => {
+          if (exp.syntaxNode.parent) {
+            const value = TreeUtils.findFirstNamedChildOfType(
+              "upper_case_identifier",
+              exp.syntaxNode.parent,
+            )?.text;
+
+            if (value) {
+              exposedValues.push({
+                module,
+                value: exp.name,
+                valueToImport: `${value}(..)`,
+                package: tree.maintainerAndPackageName,
+                type: "UnionConstructor",
+                node: exp.syntaxNode,
+              });
+            }
+          }
+        });
+      }
+    });
+
+    return exposedValues;
+  }
+
   private static comparisonScore(source: string, target: string): number {
     let score = 0;
     while (
diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts
index 6ecf1a8..f5bcbed 100644
--- a/src/util/treeUtils.ts
+++ b/src/util/treeUtils.ts
@@ -840,6 +840,21 @@ export class TreeUtils {
 
       let definitionFromOtherFile;
       if (!definitionNode) {
+        definitionFromOtherFile = this.findImportFromImportList(
+          uri,
+          TreeUtils.findImportNameNode(tree, upperCaseQid.text)?.text ??
+            upperCaseQid.text,
+          "Module",
+          imports,
+        );
+        if (definitionFromOtherFile) {
+          return {
+            node: definitionFromOtherFile.node,
+            nodeType: "Module",
+            uri: definitionFromOtherFile.fromUri,
+          };
+        }
+
         definitionFromOtherFile = this.findImportFromImportList(
           uri,
           upperCaseQid.text,
@@ -958,18 +973,45 @@ export class TreeUtils {
       );
 
       if (!topLevelDefinitionNode) {
-        const definitionFromOtherFile = this.findImportFromImportList(
+        // Get the full module name and handle an import alias if there is one
+        const endPos =
+          nodeAtPosition.parent.text.indexOf(nodeAtPosition.text) +
+          nodeAtPosition.text.length;
+        const moduleNameOrAlias = nodeAtPosition.parent.text.substring(
+          0,
+          endPos,
+        );
+        const moduleName =
+          TreeUtils.findImportNameNode(tree, moduleNameOrAlias)?.text ??
+          moduleNameOrAlias;
+
+        const moduleDefinitionFromOtherFile = this.findImportFromImportList(
+          uri,
+          moduleName,
+          "Module",
+          imports,
+        );
+
+        if (moduleDefinitionFromOtherFile) {
+          return {
+            node: moduleDefinitionFromOtherFile.node,
+            nodeType: "Module",
+            uri: moduleDefinitionFromOtherFile.fromUri,
+          };
+        }
+
+        const functionDefinitionFromOtherFile = this.findImportFromImportList(
           uri,
           nodeAtPosition.parent.text,
           "Function",
           imports,
         );
 
-        if (definitionFromOtherFile) {
+        if (functionDefinitionFromOtherFile) {
           return {
-            node: definitionFromOtherFile.node,
+            node: functionDefinitionFromOtherFile.node,
             nodeType: "Function",
-            uri: definitionFromOtherFile.fromUri,
+            uri: functionDefinitionFromOtherFile.fromUri,
           };
         }
       }
@@ -1090,6 +1132,37 @@ export class TreeUtils {
           };
         }
       }
+    } else if (
+      nodeAtPosition.type === "upper_case_identifier" &&
+      nodeAtPosition.parent?.type === "ERROR"
+    ) {
+      let fullModuleName = nodeAtPosition.text;
+
+      // Get fully qualified module name
+      // Ex: nodeAtPosition.text is Attributes, we need to get Html.Attributes manually
+      let currentNode = nodeAtPosition.previousNamedSibling;
+      while (
+        currentNode?.type === "dot" &&
+        currentNode.previousNamedSibling?.type === "upper_case_identifier"
+      ) {
+        fullModuleName = `${currentNode.previousNamedSibling.text}.${fullModuleName}`;
+        currentNode = currentNode.previousNamedSibling.previousNamedSibling;
+      }
+
+      const definitionFromOtherFile = this.findImportFromImportList(
+        uri,
+        TreeUtils.findImportNameNode(tree, fullModuleName)?.text ??
+          fullModuleName,
+        "Module",
+        imports,
+      );
+      if (definitionFromOtherFile) {
+        return {
+          node: definitionFromOtherFile.node,
+          nodeType: "Module",
+          uri: definitionFromOtherFile.fromUri,
+        };
+      }
     }
   }
 
@@ -1099,7 +1172,8 @@ export class TreeUtils {
   ): SyntaxNode | undefined {
     if (node.parent) {
       if (
-        node.parent.type === "value_declaration" &&
+        (node.parent.type === "value_declaration" ||
+          node.parent.type === "ERROR") &&
         node.parent.firstChild &&
         node.parent.firstChild.type === "function_declaration_left"
       ) {
@@ -1289,9 +1363,12 @@ export class TreeUtils {
     if (allImports) {
       const match = allImports.find(
         (a) =>
-          a.children.length > 1 &&
-          a.children[1].type === "upper_case_qid" &&
-          a.children[1].text === moduleName,
+          (a.children.length > 1 &&
+            a.children[1].type === "upper_case_qid" &&
+            a.children[1].text === moduleName) ||
+          (a.children.length > 2 &&
+            a.children[2].type === "as_clause" &&
+            a.children[2].lastNamedChild?.text === moduleName),
       );
       if (match) {
         return match.children[1];
@@ -1574,6 +1651,15 @@ export class TreeUtils {
           )?.parent ?? undefined;
       }
 
+      // Handle when the record and dot are not group, like the end of let expression
+      if (
+        !type &&
+        node.type === "value_qid" &&
+        node.parent.nextNamedSibling?.type === "dot"
+      ) {
+        type = node;
+      }
+
       if (type) {
         const definitionNode = TreeUtils.findDefinitionNodeByReferencingNode(
           type.firstNamedChild ? type.firstNamedChild : type,


Test patch:
diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts
index 9fd3fb5..1edb304 100644
--- a/test/completionProvider.test.ts
+++ b/test/completionProvider.test.ts
@@ -33,6 +33,8 @@ describe("CompletionProvider", () => {
   const completionProvider = new MockCompletionProvider(connectionMock, []);
   const treeParser = new SourceTreeParser();
 
+  const debug = process.argv.find((arg) => arg === "--debug");
+
   /**
    * Run completion tests on a source
    *
@@ -72,6 +74,24 @@ describe("CompletionProvider", () => {
         ? completions
         : completions.items;
 
+      if (debug && completionsList.length === 0) {
+        console.log(
+          `No completions found with context ${JSON.stringify(
+            context,
+          )}, expected completions: ${JSON.stringify(expectedCompletions)}`,
+        );
+      } else if (
+        debug &&
+        testExactCompletions === "exactMatch" &&
+        completionsList.length !== expectedCompletions.length
+      ) {
+        console.log(
+          `Wrong completions: ${JSON.stringify(
+            completionsList.map((c) => c.label),
+          )}, expected: ${JSON.stringify(expectedCompletions)}`,
+        );
+      }
+
       if (testExactCompletions === "exactMatch") {
         expect(completionsList.length).toBe(expectedCompletions.length);
       } else {
@@ -81,25 +101,33 @@ describe("CompletionProvider", () => {
       }
 
       expectedCompletions.forEach((completion) => {
-        expect(
-          completionsList.find((c) => {
-            if (typeof completion === "string") {
-              return c.label === completion;
-            } else {
-              // Compare label, detail, and text edit text
-              return (
-                c.label === completion.label &&
-                c.detail === completion.detail &&
-                c.additionalTextEdits &&
-                completion.additionalTextEdits &&
-                isDeepStrictEqual(
-                  c.additionalTextEdits[0],
-                  completion.additionalTextEdits[0],
-                )
-              );
-            }
-          }),
-        ).toBeTruthy();
+        const result = completionsList.find((c) => {
+          if (typeof completion === "string") {
+            return c.label === completion;
+          } else {
+            // Compare label, detail, and text edit text
+            return (
+              c.label === completion.label &&
+              c.detail === completion.detail &&
+              c.additionalTextEdits &&
+              completion.additionalTextEdits &&
+              isDeepStrictEqual(
+                c.additionalTextEdits[0],
+                completion.additionalTextEdits[0],
+              )
+            );
+          }
+        });
+
+        if (!result && debug) {
+          console.log(
+            `Could not find ${completion} in ${JSON.stringify(
+              completionsList,
+            )}`,
+          );
+        }
+
+        expect(result).toBeTruthy();
       });
     }
 
@@ -216,7 +244,34 @@ view model =
     await testCompletions(
       source,
       ["prop1", "prop2"],
-      "partialMatch",
+      "exactMatch",
+      "triggeredByDot",
+    );
+
+    const source2 = `
+--@ Test.elm
+module Test exposing (..)
+
+type alias Model = 
+  { prop1: String
+  , prop2: Int
+  }
+
+view : Model -> Model
+view model =
+  let
+    var : String
+    var = 
+      model.{-caret-}
+
+  in
+    model
+`;
+
+    await testCompletions(
+      source2,
+      ["prop1", "prop2"],
+      "exactMatch",
       "triggeredByDot",
     );
   });
@@ -589,8 +644,7 @@ func =
     ]);
   });
 
-  it("Module name with caret after dot should have completions", async () => {
-    // TODO: Add auto import completions for modules and module values
+  it("Imported modules should have fully qualified completions", async () => {
     const source = `
 --@ Data/User.elm
 module Data.User exposing (..)
@@ -605,7 +659,7 @@ module Test exposing (..)
 import Data.User
 
 test = 
-  Data.{-caret-}
+  {-caret-}
 `;
 
     await testCompletions(
@@ -631,7 +685,7 @@ module Test exposing (..)
 import Data.User
 
 test = 
-  Data.User.{-caret-}
+  Da{-caret-}
 `;
 
     await testCompletions(
@@ -650,15 +704,41 @@ module Page exposing (..)
 type Page = Home | Away
 
 --@ Test.elm
+module Test exposing (..)
+
 import Page
 
 defaultPage = 
   Page.{-caret-}
+
+func = ""
 `;
 
     await testCompletions(
       source,
-      ["Page.Home", "Page.Away"],
+      ["Home", "Away"],
+      "partialMatch",
+      "triggeredByDot",
+    );
+
+    const source2 = `
+--@ Page.elm
+module Page exposing (..)
+
+type Page = Home | Away
+
+--@ Test.elm
+module Test exposing (..)
+
+import Page
+
+defaultPage = 
+  Page.{-caret-}
+    `;
+
+    await testCompletions(
+      source2,
+      ["Home", "Away"],
       "partialMatch",
       "triggeredByDot",
     );
@@ -717,4 +797,191 @@ viewUser : U{-caret-}
 
     await testCompletions(source, ["User"]);
   });
+
+  it("Possible submodule completions", async () => {
+    const source = `
+--@ Module/Submodule.elm
+module Module.Submodule exposing (..)
+
+func = ""
+
+--@ Module/Submodule/AnotherSubmodule.elm
+module Module.Submodule.AnotherSubmodule exposing (..)
+
+func = ""
+
+--@ Test.elm
+module Test exposing (..)
+
+test : Module.{-caret-}
+`;
+
+    await testCompletions(
+      source,
+      ["Submodule", "Submodule.AnotherSubmodule"],
+      "exactMatch",
+      "triggeredByDot",
+    );
+
+    const source2 = `
+--@ Module/Submodule.elm
+module Module.Submodule exposing (..)
+
+func = ""
+
+--@ Module/Submodule/AnotherSubmodule.elm
+module Module.Submodule.AnotherSubmodule exposing (..)
+
+func = ""
+
+--@ Test.elm
+module Test exposing (..)
+
+test = Module.Submodule.{-caret-}
+    `;
+
+    await testCompletions(
+      source2,
+      ["AnotherSubmodule"],
+      "exactMatch",
+      "triggeredByDot",
+    );
+
+    const source3 = `
+--@ Module/Submodule.elm
+module Module.Submodule exposing (..)
+
+func = ""
+
+--@ Module/Submodule/AnotherSubmodule.elm
+module Module.Submodule.AnotherSubmodule exposing (..)
+
+func = ""
+
+--@ Test.elm
+module Test exposing (..)
+
+test = Module.sub{-caret-}
+    `;
+
+    await testCompletions(
+      source3,
+      ["Submodule", "Submodule.AnotherSubmodule"],
+      "exactMatch",
+    );
+  });
+
+  it("Imported qualified modules should have value completions", async () => {
+    const source = `
+--@ Module.elm
+module Module exposing (..)
+
+func = ""
+
+type Msg = Msg1 | Msg2
+
+type alias Model = { field : String }
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module
+
+test = div [] [ Module.{-caret-} ]
+`;
+
+    await testCompletions(
+      source,
+      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      "exactMatch",
+      "triggeredByDot",
+    );
+
+    const source2 = `
+--@ Module.elm
+module Module exposing (..)
+
+func = ""
+
+type Msg = Msg1 | Msg2
+
+type alias Model = { field : String }
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module
+
+test = Module.fu{-caret-}
+`;
+
+    await testCompletions(
+      source2,
+      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      "exactMatch",
+    );
+
+    const source3 = `
+--@ Module.elm
+module Module exposing (..)
+
+func = ""
+
+type Msg = Msg1 | Msg2
+
+type alias Model = { field : String }
+
+--@ Test.elm
+module Test exposing (..)
+
+import Module
+
+test = Module.{-caret-}
+`;
+
+    await testCompletions(
+      source3,
+      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      "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 = `
+--@ Other.elm
+module Other exposing (..)
+
+func = ""
+
+type alias Data = { prop : String }
+
+--@ Test.elm
+module Test exposing (..)
+
+import Other
+
+type alias Model = 
+{ prop1: String
+, prop2: Int
+}
+
+view : Model -> Model
+view model =
+  let
+    var = Other.{-caret-}
+
+    test = model.prop1
+  in
+    model
+`;
+
+    await testCompletions(
+      source,
+      ["func", "Data"],
+      "exactMatch",
+      "triggeredByDot",
+    );
+  });
 });