Repository: elm-tooling/elm-language-server
Pull request #295 (2020-06-21 02:12:54)

Add function completion for used but not declared function
https://github.com/elm-tooling/elm-language-server/issues/183

This needs tests


Patch:
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 75d5336..89a8610 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -43,7 +43,6 @@ interface ICompletionOptions {
 
 export class CompletionProvider {
   private qidRegex = /[a-zA-Z0-9.]+/;
-  private qidAtStartOfLineRegex = /^[a-zA-Z0-9 .]*$/;
 
   constructor(private connection: IConnection, elmWorkspaces: IElmWorkspace[]) {
     connection.onCompletion(
@@ -96,9 +95,7 @@ export class CompletionProvider {
 
       const previousWord = this.findPreviousWord(currentCharacter, targetLine);
 
-      const isAtStartOfLine = this.qidAtStartOfLineRegex.test(
-        targetLine.slice(0, params.position.character - 1),
-      );
+      const isAtStartOfLine = replaceRange.start.character === 0;
 
       const targetWord = targetLine.substring(
         replaceRange.start.character,
@@ -140,6 +137,60 @@ export class CompletionProvider {
             sortPrefix: "a",
           }),
         ];
+      } else if (isAtStartOfLine) {
+        const topLevelFunctions = TreeUtils.findAllTopLevelFunctionDeclarations(
+          tree,
+        );
+
+        const exposedValues = TreeUtils.descendantsOfType(
+          tree.rootNode,
+          "exposed_value",
+        );
+
+        const possibleMissingImplementations = TreeUtils.descendantsOfType(
+          tree.rootNode,
+          "function_call_expr",
+        )
+          .filter((a) => a.firstChild && !a.firstChild.text.includes("."))
+          .filter(
+            (a) =>
+              !exposedValues.some(
+                (b) => b.firstChild?.text === a.firstChild?.text,
+              ),
+          )
+          .filter(
+            (a) =>
+              !topLevelFunctions?.some(
+                (b) => b.firstChild?.text === a.firstChild?.text,
+              ),
+          );
+
+        const snippetsFroMissingImplementations = possibleMissingImplementations.map(
+          (a) =>
+            this.createSnippet(
+              "func " + a.firstChild!.text,
+              [
+                a.firstChild!.text + " : ${1:ArgumentType} -> ${2:ReturnType}",
+                a.firstChild!.text + " ${3:arguments} =",
+                "    ${4}",
+              ],
+              "Function with type annotation",
+            ),
+        );
+
+        return [
+          ...snippetsFroMissingImplementations,
+          ...possibleMissingImplementations.map((a) =>
+            this.createCompletion({
+              kind: CompletionItemKind.Text,
+              label: a.firstChild!.text,
+              range: replaceRange,
+              sortPrefix: "a",
+            }),
+          ), // Add plain text recommendations
+          ...this.getKeywordsStartOfLine(),
+          ...this.createSnippetsStartOfLine(),
+        ];
       } else if (previousWord && previousWord === "module") {
         return undefined;
       } else if (
@@ -304,8 +355,8 @@ export class CompletionProvider {
         ),
       );
 
-      completions.push(...this.createSnippets());
-      completions.push(...this.getKeywords());
+      completions.push(...this.createSnippetsInline());
+      completions.push(...this.getKeywordsInline());
 
       const possibleImportCompletions = this.getPossibleImports(
         replaceRange,
@@ -544,7 +595,7 @@ export class CompletionProvider {
     moduleDefinition = false,
   ): CompletionItem[] {
     const completions: CompletionItem[] = [];
-    const topLevelFunctions = TreeUtils.findAllTopLeverFunctionDeclarations(
+    const topLevelFunctions = TreeUtils.findAllTopLevelFunctionDeclarations(
       tree,
     );
     const sortPrefix = "b";
@@ -1079,18 +1130,8 @@ export class CompletionProvider {
     };
   }
 
-  private createSnippets(): CompletionItem[] {
+  private createSnippetsInline(): CompletionItem[] {
     return [
-      this.createSnippet(
-        "module",
-        "module ${1:Name} exposing (${2:..})",
-        "Module definition",
-      ),
-      this.createSnippet(
-        "import",
-        "import ${1:Name} exposing (${2:..})",
-        "Unqualified import",
-      ),
       this.createSnippet(
         "of",
         ["of", "   $0"],
@@ -1107,6 +1148,36 @@ export class CompletionProvider {
         [" if ${1:expression} then", "    ${2}", " else", "    ${3}"],
         "If-Else statement",
       ),
+      this.createSnippet(
+        "record update",
+        ["{ ${1:recordName} | ${2:key} = ${3} }"],
+        "Update record",
+      ),
+      this.createSnippet(
+        "anonymous",
+        ["\\ ${1:argument} -> ${1:argument}"],
+        "Anonymous function",
+      ),
+      this.createSnippet(
+        "let in",
+        ["let", "    ${1}", "in", "${0}"],
+        "Let expression",
+      ),
+    ];
+  }
+
+  private createSnippetsStartOfLine(): CompletionItem[] {
+    return [
+      this.createSnippet(
+        "module",
+        "module ${1:Name} exposing (${2:..})",
+        "Module definition",
+      ),
+      this.createSnippet(
+        "import",
+        "import ${1:Name} exposing (${2:..})",
+        "Unqualified import",
+      ),
       this.createSnippet("comment", ["{-", "${0}", "-}"], "Multi-line comment"),
       this.createSnippet(
         "record",
@@ -1128,16 +1199,6 @@ export class CompletionProvider {
         ],
         "Type alias",
       ),
-      this.createSnippet(
-        "record update",
-        ["{ ${1:recordName} | ${2:key} = ${3} }"],
-        "Update record",
-      ),
-      this.createSnippet(
-        "anonymous",
-        ["\\ ${1:argument} -> ${1:argument}"],
-        "Anonymous function",
-      ),
       this.createSnippet(
         "type",
         ["type ${1:Typename}", "    = ${2:Value1}", "    | ${3:Value2}"],
@@ -1157,11 +1218,6 @@ export class CompletionProvider {
         ],
         "Function with type annotation",
       ),
-      this.createSnippet(
-        "let in",
-        ["let", "    ${1}", "in", "${0}"],
-        "Let expression",
-      ),
       this.createSnippet(
         "update",
         [
@@ -1555,7 +1611,7 @@ export class CompletionProvider {
     };
   }
 
-  private getKeywords(): CompletionItem[] {
+  private getKeywordsInline(): CompletionItem[] {
     return [
       this.createKeywordCompletion("if"),
       this.createKeywordCompletion("then"),
@@ -1563,10 +1619,17 @@ export class CompletionProvider {
       this.createKeywordCompletion("let"),
       this.createKeywordCompletion("in"),
       this.createKeywordCompletion("case"),
-      this.createKeywordCompletion("type"),
       this.createKeywordCompletion("alias"),
-      this.createKeywordCompletion("import"),
       this.createKeywordCompletion("exposing"),
     ];
   }
+
+  private getKeywordsStartOfLine(): CompletionItem[] {
+    return [
+      this.createKeywordCompletion("type"),
+      this.createKeywordCompletion("import"),
+      this.createKeywordCompletion("module"),
+      this.createKeywordCompletion("port"),
+    ];
+  }
 }
diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts
index f5bcbed..13fdb2f 100644
--- a/src/util/treeUtils.ts
+++ b/src/util/treeUtils.ts
@@ -563,7 +563,7 @@ export class TreeUtils {
     }
   }
 
-  public static findAllTopLeverFunctionDeclarations(
+  public static findAllTopLevelFunctionDeclarations(
     tree: Tree,
   ): SyntaxNode[] | undefined {
     const result = tree.rootNode.children.filter(


Test patch:
diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts
index 1edb304..487b5bb 100644
--- a/test/completionProvider.test.ts
+++ b/test/completionProvider.test.ts
@@ -208,8 +208,8 @@ type alias Model =
 view : Model
 view =
   let
-    func : Model
-    func = 
+    testFunc : Model
+    testFunc = 
       { p{-caret-} }
 
   in
@@ -519,22 +519,22 @@ type alias TestType =
 module Test exposing (..)
 
 {-caret-} 
-func =
+testFunc =
   ""
     `;
 
-    await testCompletions(source, ["func"], "exactMatch");
+    await testCompletions(source, ["testFunc"], "exactMatch");
 
     const source2 = `
 --@ Test.elm
 module Test exposing (..)
 
-func : String
-f{-caret-} =  
+testFunc : String
+t{-caret-} =  
   ""
 `;
 
-    await testCompletions(source2, ["func"], "exactMatch");
+    await testCompletions(source2, ["testFunc"], "exactMatch");
   });
 
   it("Case branch variables should have completions", async () => {
@@ -544,8 +544,8 @@ module Test exposing (..)
 
 type Msg = Msg1 String
 
-func : Msg -> String
-func msg = 
+testFunc : Msg -> String
+testFunc msg = 
   case msg of
     Msg1 str ->
       s{-caret-}
@@ -568,8 +568,8 @@ type alias TestType = {prop : String}
 --@ Test.elm
 module Test exposing (..)
 
-func : String
-func = 
+testFunc : String
+testFunc = 
   {-caret-}
 `;
 
@@ -605,8 +605,8 @@ type Msg = Msg1 | Msg2
 --@ Test.elm
 module Test exposing (..)
 
-func : String
-func = 
+testFunc : String
+testFunc = 
   {-caret-}
 `;
 
@@ -876,7 +876,7 @@ test = Module.sub{-caret-}
 --@ Module.elm
 module Module exposing (..)
 
-func = ""
+testFunc = ""
 
 type Msg = Msg1 | Msg2
 
@@ -892,7 +892,7 @@ test = div [] [ Module.{-caret-} ]
 
     await testCompletions(
       source,
-      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      ["testFunc", "Msg", "Msg1", "Msg2", "Model"],
       "exactMatch",
       "triggeredByDot",
     );
@@ -901,7 +901,7 @@ test = div [] [ Module.{-caret-} ]
 --@ Module.elm
 module Module exposing (..)
 
-func = ""
+testFunc = ""
 
 type Msg = Msg1 | Msg2
 
@@ -917,7 +917,7 @@ test = Module.fu{-caret-}
 
     await testCompletions(
       source2,
-      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      ["testFunc", "Msg", "Msg1", "Msg2", "Model"],
       "exactMatch",
     );
 
@@ -925,7 +925,7 @@ test = Module.fu{-caret-}
 --@ Module.elm
 module Module exposing (..)
 
-func = ""
+testFunc = ""
 
 type Msg = Msg1 | Msg2
 
@@ -941,7 +941,7 @@ test = Module.{-caret-}
 
     await testCompletions(
       source3,
-      ["func", "Msg", "Msg1", "Msg2", "Model"],
+      ["testFunc", "Msg", "Msg1", "Msg2", "Model"],
       "exactMatch",
       "triggeredByDot",
     );
@@ -953,7 +953,7 @@ test = Module.{-caret-}
 --@ Other.elm
 module Other exposing (..)
 
-func = ""
+testFunc = ""
 
 type alias Data = { prop : String }
 
@@ -979,9 +979,79 @@ view model =
 
     await testCompletions(
       source,
-      ["func", "Data"],
+      ["testFunc", "Data"],
       "exactMatch",
       "triggeredByDot",
     );
   });
+
+  it("Complete used but undefined functions", async () => {
+    const source = `
+module Main exposing (..)
+
+import Json.Encode
+
+
+type alias Scope =
+  { user : User
+  }
+
+
+type alias User =
+  { name : String
+  }
+
+
+encodeScope : Scope -> Json.Encode.Value
+encodeScope scope =
+  Json.Encode.object
+  [ ( "tag", Json.Encode.string "Scope" )
+  , ( "user", encodeUser scope.user )
+  ]
+
+{-caret-}
+  `;
+
+    await testCompletions(
+      source,
+      ["encodeUser", "func encodeUser"],
+      "partialMatch",
+      "normal",
+    );
+  });
+
+  it("Complete used but undefined functions, remove funcs exported from other file", async () => {
+    const source = `
+module Main exposing (..)
+
+import Json.Encode exposing (string)
+
+
+type alias Scope =
+  { user : User
+  }
+
+
+type alias User =
+  { name : String
+  }
+
+
+encodeScope : Scope -> Json.Encode.Value
+encodeScope scope =
+  Json.Encode.object
+  [ ( "tag", string "Scope" )
+  , ( "user", encodeUser scope.user )
+  ]
+
+{-caret-}
+  `;
+
+    await testCompletions(
+      source,
+      ["encodeUser", "func encodeUser"],
+      "partialMatch",
+      "normal",
+    );
+  });
 });