Repository: elm-tooling/elm-language-server
Pull request #818 (2022-11-01 23:00:36)

Bump @types/node from 18.8.1 to 18.11.9
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.8.1 to 18.11.9.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=18.8.1&new-version=18.11.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/README.md b/README.md
index b5ae2f4..44dea86 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ This is the language server implementation for the Elm programming language.
   - [Alternative: Install with Nix](#alternative-install-with-nix)
 - [Requirements](#requirements)
 - [Configuration](#configuration)
+  - [Linting](#linting)
 - [Features](#features)
 - [Server Settings](#server-settings)
 - [Editor Support](#editor-support)
@@ -103,6 +104,10 @@ If all you had in `elm-tooling.json` was `"entrypoints"`, you can safely remove
 
 Currently, no configuration at all is needed.
 
+### Linting
+
+The Elm Language Server has built-in support for linting. Check out the [documentation](docs/linting.md) for configuring the linter. 
+
 ## Features
 
 Supports Elm 0.19 and up
diff --git a/docs/linting.md b/docs/linting.md
new file mode 100644
index 0000000..7da1b57
--- /dev/null
+++ b/docs/linting.md
@@ -0,0 +1,340 @@
+# Linting Configuration
+
+Elm Language Server has built-in support for linting.
+All linting rules are enabled by default. If you want to disable a rule you 
+can disable them by placing a configuration file in your project.
+
+The linting system was based on Elm-Analyse, and the cofiguration:
+
+`<workspace-root>/elm-analyse.json`
+
+### Example Configuration
+
+This is the default configuration, with all checks enabled:
+
+```json
+{
+  "checks": {
+    "BooleanCase": true,
+    "DropConcatOfLists": true,
+    "DropConsOfItemAndList": true,
+    "NoUncurriedPrefix": true,
+    "MapNothingToNothing": true,
+    "SingleFieldRecord": true,
+    "UnnecessaryListConcat": true,
+    "UnnecessaryPortModule": true,
+    "UnusedImport": true,
+    "UnusedImportAlias": true,
+    "UnusedImportedVariable": true,
+    "UnusedPatternVariable": true,
+    "UnusedTopLevel": true,
+    "UnusedTypeAlias": true,
+    "UnusedValueConstructor": true,
+    "UnusedVariable": true,
+    "UseConsOverConcat": true,
+    "MissingTypeAnnotation": true
+  },
+  "excludedPaths": []
+}
+```
+
+## Exclude paths
+
+You can exclude certain files and folders from being linted by adding their path to
+`excludedPaths` in `elm-analyse.json`.
+
+### Example:
+
+```json
+"excludedPaths" : [
+	"src/Vendor",
+	"src/App/FileThatShouldNotBeLinted.elm"
+],
+```
+
+## Checks
+
+### `BooleanCase`
+
+If you case over a boolean value, it is more idiomatic to use an if-expression.
+
+#### Example rule violation:
+
+```elm
+thing : Boolean -> String
+thing x =
+    case x of
+        True ->
+            "Hello"
+        False ->
+            "Goodbye"
+```
+
+### `DropConcatOfLists`
+
+If you concatenate two list literals `[...] ++ [...]`, then you can merge them into one list.
+
+#### Example rule violation:
+
+```elm
+foo : List Int
+foo =
+    [ 1, 2, 3 ] ++ [ 4, 5, 6]
+```
+
+
+### `DropConsOfItemAndList`
+
+If you cons an item to a literal list (x :: [1, 2, 3]), then you can just put the item into the list.
+
+#### Example rule violation:
+
+```elm
+foo : List Int
+foo =
+    1 :: [ 2, 3, 4]
+```
+
+### `NoUncurriedPrefix`
+
+It's not needed to use an operator in prefix notation when you apply both arguments directly.
+
+#### Example rule violation:
+
+```elm
+hello : String
+hello =
+    (++) "Hello " "World"
+```
+
+
+### `MapNothingToNothing`
+
+Do not map a `Nothing` to `Nothing` with a case expression. Use `andThen` or `map` instead.
+
+#### Example rule violation:
+
+```elm
+greet : Maybe String -> Maybe String
+greet x =
+    case x of
+        Nothing ->
+            Nothing
+        Just x ->
+            Just ("Hello " ++ x)
+```
+
+
+### `SingleFieldRecord`
+
+Using a record is obsolete if you only plan to store a single field in it.
+
+#### Example rule violation:
+
+```elm
+type Model =
+    Model { input : String }
+```
+
+
+### `UnnecessaryListConcat`
+
+You should not use `List.concat` to concatenate literal lists. Just join the lists together.
+
+#### Example rule violation:
+
+```elm
+foo : List Int
+foo =
+    List.concat [ [ 1, 2 ,3 ], [ a, b, c] ]
+```
+
+### `UnnecessaryPortModule`
+
+Don't use the port keyword if you do not need it.
+
+#### Example rule violation:
+
+```elm
+port module Foo exposing (notAPort)
+
+notAPort : Int
+notAPort = 1
+```
+
+### `UnusedImport`
+
+Imports that have no meaning should be removed.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (main)
+
+import Html exposing (Html, text)
+import SomeOtherModule
+
+main : Html a
+main =
+    text "Hello"
+```
+
+### `UnusedImportAlias`
+
+You defined an alias for an import (import Foo as F), but it turns out you never use it.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (main)
+
+import Html as H exposing (Html, text)
+
+main : Html a
+main =
+    text "Hello"
+```
+
+### `UnusedImportedVariable`
+
+When a function is imported from a module but is unused, it is better to remove it.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (thing)
+
+import Html exposing (Html, div, text)
+
+main : Html a
+main =
+    text "Hello World!"
+```
+
+### `UnusedPatternVariable`
+
+Variables in pattern matching that are unused should be replaced with '_' or left
+out completely.
+
+#### Example rule violation:
+
+```elm
+greetRecord {name, age} = "Hello " ++ name
+
+greetTuple (name, age) = "Hello " ++ name
+
+greetConstructor (Person name age) = "Hello " ++ name
+```
+
+### `UnusedTopLevel`
+
+Functions and values that are unused in a module and not exported are dead code.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (thing)
+
+thing : Int
+thing =
+    1
+
+unusedThing : String -> String
+unusedThing x =
+    "Hello " ++ x
+```
+
+
+### `UnusedTypeAlias`
+
+You defined a type alias, but you do not use it in any signature or expose it.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (main)
+
+import Html exposing (Html, text, Html)
+
+type alias SomeUnusedThing =
+    { name : String }
+
+main : Html a
+main =
+    text "Hello World"
+```
+
+### `UnusedValueConstructor`
+
+Value constructors which are not exposed and used should be eliminated.
+
+#### Example rule violation:
+
+```elm
+module Greet exposing (Color, red, blue)
+
+type Color
+    = Blue
+    | Red
+    | Green
+
+red : Color
+red = Red
+
+blue : Color
+blue = Blue
+```
+
+
+### `UnusedVariable`
+
+Variables that are not used could be removed or marked as '_'.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (foo)
+
+foo : String -> Int
+foo x =
+    1
+```
+
+
+### `UseConsOverConcat`
+
+If you concatenate two lists, but the right hand side is a single element list, then you should use the cons operator `::`.
+
+#### Example rule violation:
+
+```elm
+foo : List String
+foo =
+    [ "a" ] ++ [ "b", "c" ]
+```
+
+
+### `MissingTypeAnnotation`
+
+Type annotations are required on top-level declarations.
+
+#### Example rule violation:
+
+```elm
+module Foo exposing (foo)
+
+foo = 1
+```
+
+_Note: This check was called "NoTopLevelSignature" in Elm-Analyse._
+
+
+## Compatibility with Elm Analyse
+
+The linter was inspired by the tool [Elm-Analyse](https://stil4m.github.io/elm-analyse/) by [Mats Stijlaart](https://github.com/stil4m), and existing elm-analyse configuration should be fully compatible with ElmLS.
+
+While Elm Language Server does not use Elm-Analyse direct, the check names and descriptions are inspired by it.
+
+We have deliberately excluded some checks that are supported by Elm-Analyse. Either they did not fit well with ElmLS or the issues they check are automatically fixed by elm-format.
+
+ElmLS does also not support the `TriggerWords` property from Elm-Analyses and will just ignore it.
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e7bc7e4..86116b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1355,9 +1355,9 @@
       }
     },
     "node_modules/@types/node": {
-      "version": "18.8.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.1.tgz",
-      "integrity": "sha512-vuYaNuEIbOYLTLUAJh50ezEbvxrD43iby+lpUA2aa148Nh5kX/AVO/9m1Ahmbux2iU5uxJTNF9g2Y+31uml7RQ==",
+      "version": "18.11.9",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
       "dev": true
     },
     "node_modules/@types/prettier": {
@@ -7810,9 +7810,9 @@
       }
     },
     "@types/node": {
-      "version": "18.8.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.1.tgz",
-      "integrity": "sha512-vuYaNuEIbOYLTLUAJh50ezEbvxrD43iby+lpUA2aa148Nh5kX/AVO/9m1Ahmbux2iU5uxJTNF9g2Y+31uml7RQ==",
+      "version": "18.11.9",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
       "dev": true
     },
     "@types/prettier": {
diff --git a/src/compiler/typeChecker.ts b/src/compiler/typeChecker.ts
index c48effc..1dd34d3 100644
--- a/src/compiler/typeChecker.ts
+++ b/src/compiler/typeChecker.ts
@@ -95,6 +95,7 @@ export interface TypeChecker {
     moduleNameOrAlias: string,
     sourceFile: ISourceFile,
   ) => SyntaxNode[];
+  getSymbolsInScope(node: SyntaxNode, sourceFile: ISourceFile): ISymbol[];
 }
 
 export function createTypeChecker(program: IProgram): TypeChecker {
@@ -124,6 +125,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
     getDiagnosticsAsync,
     getSuggestionDiagnostics,
     findImportModuleNameNodes,
+    getSymbolsInScope,
   };
 
   return typeChecker;
@@ -890,6 +892,23 @@ export function createTypeChecker(program: IProgram): TypeChecker {
     );
   }
 
+  function getSymbolsInScope(
+    node: SyntaxNode,
+    sourceFile: ISourceFile,
+  ): ISymbol[] {
+    const symbols: ISymbol[] = [];
+
+    let targetScope: SyntaxNode | null = node;
+    while (targetScope != null) {
+      sourceFile.symbolLinks
+        ?.get(targetScope)
+        ?.forEach((symbol) => symbols.push(symbol));
+      targetScope = targetScope.parent;
+    }
+
+    return symbols;
+  }
+
   function checkNode(node: SyntaxNode): void {
     if (checkedNodes.has(node.id)) {
       return;
diff --git a/src/providers/codeAction/extractFunctionCodeAction.ts b/src/providers/codeAction/extractFunctionCodeAction.ts
index 56a5cba..d69f1de 100644
--- a/src/providers/codeAction/extractFunctionCodeAction.ts
+++ b/src/providers/codeAction/extractFunctionCodeAction.ts
@@ -129,6 +129,9 @@ CodeActionProvider.registerRefactorAction(refactorName, {
 
     const args: SyntaxNode[] = [];
 
+    const nodeParent = node.parent;
+    const imports = checker.getAllImports(params.sourceFile);
+
     // Get the list of references that won't be visible
     node
       .descendantsOfType(["value_expr", "record_base_identifier"])
@@ -142,27 +145,17 @@ CodeActionProvider.registerRefactorAction(refactorName, {
           return;
         }
 
-        const ref = checker.findDefinition(
-          val.firstNamedChild?.firstNamedChild ?? val.firstNamedChild ?? val,
-          params.sourceFile,
-        );
-
-        // Reference in other file will be visible
-        if (ref.symbol?.node.tree.uri !== params.sourceFile.uri) {
-          return;
-        }
-
-        // Reference inside self will be visible
-        if (
-          node.startIndex <= ref.symbol.node.startIndex &&
-          node.endIndex >= ref.symbol.node.endIndex
-        ) {
-          return;
+        // If we find it in the scope we are extracting, it should not be a arg
+        let scope: SyntaxNode | null = val;
+        while (scope && scope.id !== nodeParent?.id) {
+          if (params.sourceFile.symbolLinks?.get(scope)?.get(val.text)) {
+            return;
+          }
+          scope = scope.parent;
         }
 
-        let scope: SyntaxNode | null = targetScope;
-
-        // Check the target scope and parent scopes for the reference
+        // If we find it in the target scope, it should not be an arg
+        scope = targetScope;
         while (scope) {
           if (params.sourceFile.symbolLinks?.get(scope)?.get(val.text)) {
             return;
@@ -170,6 +163,11 @@ CodeActionProvider.registerRefactorAction(refactorName, {
           scope = scope.parent;
         }
 
+        // If we find it in imports, it should not be an arg
+        if (imports.getVar(val.text).length > 0) {
+          return;
+        }
+
         args.push(val);
       });
 
diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts
index 4fc9122..8c08884 100644
--- a/src/providers/codeActionProvider.ts
+++ b/src/providers/codeActionProvider.ts
@@ -280,6 +280,14 @@ export class CodeActionProvider {
 
   protected onCodeAction(params: ICodeActionParams): CodeAction[] | undefined {
     this.connection.console.info("A code action was requested");
+
+    // Don't try to get them if there is a top level parse error
+    // It can create many diagnostics which cause a huge performance hit
+    // Code actions aren't useful in this case anyways
+    if (params.sourceFile.tree.rootNode.type === "ERROR") {
+      return [];
+    }
+
     const make = this.elmMake.onCodeAction(params);
 
     const results: (ICodeAction | IRefactorCodeAction)[] = [];
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 0db11f1..37ae5c0 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -263,7 +263,8 @@ export class CompletionProvider {
           )?.namedChildren.map((n) => n.text) ?? [];
 
         return this.getSameFileTopLevelCompletions(
-          tree,
+          checker,
+          sourceFile,
           replaceRange,
           true,
         ).filter((completion) => !exposingList.includes(completion.label));
@@ -375,10 +376,19 @@ export class CompletionProvider {
       }
 
       completions.push(
-        ...this.getSameFileTopLevelCompletions(tree, replaceRange),
+        ...this.getSameFileTopLevelCompletions(
+          checker,
+          sourceFile,
+          replaceRange,
+        ),
       );
       completions.push(
-        ...this.findDefinitionsForScope(nodeAtPosition, tree, replaceRange),
+        ...this.findDefinitionsForScope(
+          checker,
+          nodeAtPosition,
+          sourceFile,
+          replaceRange,
+        ),
       );
 
       completions.push(
@@ -653,49 +663,35 @@ export class CompletionProvider {
   }
 
   private getSameFileTopLevelCompletions(
-    tree: Tree,
+    checker: TypeChecker,
+    sourceFile: ISourceFile,
     range: Range,
     moduleDefinition = false,
   ): CompletionItem[] {
     const completions: CompletionItem[] = [];
-    const topLevelFunctions =
-      TreeUtils.findAllTopLevelFunctionDeclarations(tree);
     const sortPrefix = "b";
-    // Add functions
-    if (topLevelFunctions) {
-      const declarations = topLevelFunctions.filter(
-        (a) =>
-          a.firstNamedChild !== null &&
-          a.firstNamedChild.type === "function_declaration_left" &&
-          a.firstNamedChild.firstNamedChild !== null &&
-          a.firstNamedChild.firstNamedChild.type === "lower_case_identifier",
-      );
-      for (const declaration of declarations) {
-        const markdownDocumentation = HintHelper.createHint(declaration);
-        completions.push(
-          this.createFunctionCompletion({
-            markdownDocumentation,
-            label: declaration.firstNamedChild!.firstNamedChild!.text,
-            range,
-            sortPrefix,
-          }),
-        );
-      }
-    }
-    // Add types
-    const typeDeclarations = TreeUtils.findAllTypeDeclarations(tree);
-    if (typeDeclarations) {
-      for (const declaration of typeDeclarations) {
-        const markdownDocumentation = HintHelper.createHint(declaration);
-        const name = TreeUtils.findFirstNamedChildOfType(
-          "upper_case_identifier",
-          declaration,
-        );
-        if (name) {
+
+    checker
+      .getSymbolsInScope(sourceFile.tree.rootNode, sourceFile)
+      .forEach((symbol) => {
+        if (symbol.type === "Function" || symbol.type === "Port") {
+          const markdownDocumentation = HintHelper.createHint(symbol.node);
+          completions.push(
+            this.createFunctionCompletion({
+              markdownDocumentation,
+              label: symbol.name,
+              range,
+              sortPrefix,
+            }),
+          );
+        }
+
+        if (symbol.type === "Type") {
+          const markdownDocumentation = HintHelper.createHint(symbol.node);
           completions.push(
             this.createTypeCompletion({
               markdownDocumentation,
-              label: name.text,
+              label: symbol.name,
               range,
               sortPrefix,
             }),
@@ -704,56 +700,36 @@ export class CompletionProvider {
             completions.push(
               this.createTypeCompletion({
                 markdownDocumentation,
-                label: `${name.text}(..)`,
+                label: `${symbol.name}(..)`,
                 range,
                 sortPrefix,
               }),
             );
           }
-        }
-        // Add types constructors
-        const unionVariants = TreeUtils.descendantsOfType(
-          declaration,
-          "union_variant",
-        );
-        for (const unionVariant of unionVariants) {
-          const unionVariantName = TreeUtils.findFirstNamedChildOfType(
-            "upper_case_identifier",
-            unionVariant,
-          );
-          if (unionVariantName) {
+
+          symbol.constructors?.forEach((unionVariant) => {
             completions.push(
               this.createUnionConstructorCompletion({
-                label: unionVariantName.text,
+                label: unionVariant.name,
                 range,
                 sortPrefix,
               }),
             );
-          }
+          });
         }
-      }
-    }
-    // Add alias types
-    const typeAliasDeclarations = TreeUtils.findAllTypeAliasDeclarations(tree);
-    if (typeAliasDeclarations) {
-      for (const declaration of typeAliasDeclarations) {
-        const markdownDocumentation = HintHelper.createHint(declaration);
-        const name = TreeUtils.findFirstNamedChildOfType(
-          "upper_case_identifier",
-          declaration,
-        );
-        if (name) {
+
+        if (symbol.type === "TypeAlias") {
+          const markdownDocumentation = HintHelper.createHint(symbol.node);
           completions.push(
             this.createTypeAliasCompletion({
               markdownDocumentation,
-              label: name.text,
+              label: symbol.name,
               range,
               sortPrefix,
             }),
           );
         }
-      }
-    }
+      });
 
     return completions;
   }
@@ -971,128 +947,92 @@ export class CompletionProvider {
   }
 
   private findDefinitionsForScope(
+    checker: TypeChecker,
     node: SyntaxNode,
-    tree: Tree,
+    sourceFile: ISourceFile,
     range: Range,
   ): CompletionItem[] {
     const result: CompletionItem[] = [];
-    const sortPrefix = "a";
-    if (node.parent) {
-      if (node.parent.type === "let_in_expr") {
-        node.parent.children.forEach((nodeToProcess) => {
-          if (
-            nodeToProcess.type === "value_declaration" &&
-            nodeToProcess.firstNamedChild?.type ===
-              "function_declaration_left" &&
-            nodeToProcess.firstNamedChild.firstNamedChild?.type ===
-              "lower_case_identifier"
-          ) {
-            const markdownDocumentation =
-              HintHelper.createHintFromDefinitionInLet(nodeToProcess);
-            result.push(
-              this.createFunctionCompletion({
-                markdownDocumentation,
-                label: nodeToProcess.firstNamedChild.firstNamedChild.text,
-                range,
-                sortPrefix,
-              }),
-            );
-          }
-        });
-      }
-      if (node.parent.type === "case_of_branch") {
-        const pattern = node.parent.childForFieldName("pattern");
-
-        if (pattern) {
-          const caseBranchVariableNodes =
-            pattern.descendantsOfType("lower_pattern");
-          if (caseBranchVariableNodes) {
-            caseBranchVariableNodes.forEach((a) => {
-              const markdownDocumentation =
-                HintHelper.createHintFromDefinitionInCaseBranch();
-              result.push(
-                this.createVariableCompletion({
-                  markdownDocumentation,
-                  label: a.text,
-                  range,
-                  sortPrefix,
-                }),
-              );
-            });
-          }
+
+    checker.getSymbolsInScope(node, sourceFile).forEach((symbol, i) => {
+      // getSymbolsInScope returns ths symbols in order of inner scope to outer scope,
+      // so we add the index to the sort order so the variables that are "closer" to the position are sorted first
+      const sortPrefix = `a${i}`;
+      if (symbol.type === "Function") {
+        // Only get let functions here
+        if (symbol.node.parent?.parent?.parent) {
+          const markdownDocumentation =
+            HintHelper.createHintFromDefinitionInLet(symbol.node);
+          result.push(
+            this.createFunctionCompletion({
+              markdownDocumentation,
+              label: symbol.name,
+              range,
+              sortPrefix: sortPrefix,
+            }),
+          );
         }
       }
-      if (
-        node.parent.type === "value_declaration" &&
-        node.parent.firstChild &&
-        node.parent.firstChild.type === "function_declaration_left"
-      ) {
-        node.parent.firstChild.children.forEach((child) => {
-          if (child.type === "lower_pattern") {
-            const markdownDocumentation =
-              HintHelper.createHintFromFunctionParameter(child);
+
+      if (symbol.type === "CasePattern") {
+        const markdownDocumentation =
+          HintHelper.createHintFromDefinitionInCaseBranch();
+        result.push(
+          this.createVariableCompletion({
+            markdownDocumentation,
+            label: symbol.name,
+            range,
+            sortPrefix,
+          }),
+        );
+      }
+
+      if (symbol.type === "FunctionParameter") {
+        const markdownDocumentation =
+          HintHelper.createHintFromFunctionParameter(symbol.node);
+        result.push(
+          this.createVariableCompletion({
+            markdownDocumentation,
+            label: symbol.name,
+            range,
+            sortPrefix,
+          }),
+        );
+
+        const parameterType = checker.findType(symbol.node);
+
+        if (parameterType.nodeType === "Record") {
+          for (const field in parameterType.fields) {
+            const hint = HintHelper.createHintForTypeAliasReference(
+              checker.typeToString(parameterType.fields[field]),
+              field,
+              symbol.name,
+            );
+
             result.push(
-              this.createVariableCompletion({
-                markdownDocumentation,
-                label: child.text,
+              this.createFieldOrParameterCompletion(
+                hint,
+                `${symbol.name}.${field}`,
                 range,
-                sortPrefix,
-              }),
+              ),
             );
-
-            const annotationTypeNode =
-              TreeUtils.getTypeOrTypeAliasOfFunctionParameter(child);
-            if (annotationTypeNode) {
-              const typeDeclarationNode = TreeUtils.findTypeAliasDeclaration(
-                tree,
-                annotationTypeNode.text,
-              );
-              if (typeDeclarationNode) {
-                const fields =
-                  TreeUtils.getAllFieldsFromTypeAlias(typeDeclarationNode);
-                if (fields) {
-                  fields.forEach((element) => {
-                    const hint = HintHelper.createHintForTypeAliasReference(
-                      element.type,
-                      element.field,
-                      child.text,
-                    );
-                    result.push(
-                      this.createFieldOrParameterCompletion(
-                        hint,
-                        `${child.text}.${element.field}`,
-                        range,
-                      ),
-                    );
-                  });
-                }
-              }
-            }
           }
-        });
+        }
       }
-      if (node.parent.type === "anonymous_function_expr") {
-        node.parent?.children.forEach((child) => {
-          if (child.type === "pattern") {
-            const lowerPatterns = child.descendantsOfType("lower_pattern");
-
-            lowerPatterns.forEach((pattern) => {
-              const markdownDocumentation =
-                HintHelper.createHintFromFunctionParameter(pattern);
-              result.push(
-                this.createVariableCompletion({
-                  markdownDocumentation,
-                  label: pattern.text,
-                  range,
-                  sortPrefix,
-                }),
-              );
-            });
-          }
-        });
+
+      if (symbol.type === "AnonymousFunctionParameter") {
+        const markdownDocumentation =
+          HintHelper.createHintFromFunctionParameter(symbol.node);
+        result.push(
+          this.createVariableCompletion({
+            markdownDocumentation,
+            label: symbol.name,
+            range,
+            sortPrefix,
+          }),
+        );
       }
-      result.push(...this.findDefinitionsForScope(node.parent, tree, range));
-    }
+    });
 
     return result;
   }
diff --git a/src/providers/handlers/moveRefactoringHandler.ts b/src/providers/handlers/moveRefactoringHandler.ts
index 3ccb1d8..e0e2eca 100644
--- a/src/providers/handlers/moveRefactoringHandler.ts
+++ b/src/providers/handlers/moveRefactoringHandler.ts
@@ -12,6 +12,7 @@ import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher";
 import { RefactorEditUtils } from "../../util/refactorEditUtils";
 import { References } from "../../compiler/references";
 import { TreeUtils } from "../../util/treeUtils";
+import * as path from "path";
 
 export class MoveRefactoringHandler {
   private connection: Connection;
@@ -45,7 +46,7 @@ export class MoveRefactoringHandler {
         const rootPath = params.program.getRootPath().fsPath;
 
         uri = uri.slice(rootPath.length + 1);
-        const index = uri.lastIndexOf("\\");
+        const index = uri.lastIndexOf(path.sep);
 
         return {
           name: uri.slice(index + 1),
diff --git a/src/util/hintHelper.ts b/src/util/hintHelper.ts
index f41ffbd..3275dc4 100644
--- a/src/util/hintHelper.ts
+++ b/src/util/hintHelper.ts
@@ -19,7 +19,8 @@ export class HintHelper {
         return this.createHintFromFieldType(node);
       } else if (node.type === "port_annotation") {
         const name = node.childForFieldName("name");
-        if (name && typeString) {
+        const typeExpression = node.childForFieldName("typeExpression");
+        if (name && typeExpression) {
           let comment = "";
           if (
             node.previousNamedSibling &&
@@ -28,7 +29,10 @@ export class HintHelper {
             comment = node.previousNamedSibling.text;
           }
 
-          return this.formatHint(`${name.text} : ${typeString}`, comment);
+          return this.formatHint(
+            `${name.text} : ${typeExpression.text}`,
+            comment,
+          );
         }
       } else {
         return this.createHintFromDefinition(node);
diff --git a/src/util/refactorEditUtils.ts b/src/util/refactorEditUtils.ts
index 080819b..a620a6f 100644
--- a/src/util/refactorEditUtils.ts
+++ b/src/util/refactorEditUtils.ts
@@ -116,6 +116,10 @@ export class RefactorEditUtils {
         valueName += "(..)";
       }
 
+      if (exposedNodes.findIndex((n) => n.text === valueName) !== -1) {
+        return;
+      }
+
       if (lastExposedNode) {
         return TextEdit.insert(
           Position.create(
diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts
index 210ebee..b1e6b8e 100644
--- a/src/util/treeUtils.ts
+++ b/src/util/treeUtils.ts
@@ -173,24 +173,6 @@ export class TreeUtils {
     return false;
   }
 
-  public static findUnionConstructor(
-    tree: Tree,
-    unionConstructorName: string,
-  ): SyntaxNode | undefined {
-    const unionVariants = TreeUtils.descendantsOfType(
-      tree.rootNode,
-      "union_variant",
-    );
-    if (unionVariants.length > 0) {
-      return unionVariants.find(
-        (a) =>
-          a.firstChild !== null &&
-          a.firstChild.type === "upper_case_identifier" &&
-          a.firstChild.text === unionConstructorName,
-      );
-    }
-  }
-
   public static findUnionConstructorCalls(
     tree: Tree,
     unionConstructorName: string,
@@ -214,64 +196,6 @@ export class TreeUtils {
     }
   }
 
-  public static findFunction(
-    syntaxNode: SyntaxNode,
-    functionName: string,
-    onlySearchTopLevel = true,
-  ): SyntaxNode | undefined {
-    const functions = onlySearchTopLevel
-      ? syntaxNode.children.filter((a) => a.type === "value_declaration")
-      : syntaxNode.descendantsOfType("value_declaration");
-
-    let ret;
-    if (functions) {
-      ret = functions
-        .map((elmFunction) =>
-          TreeUtils.findFirstNamedChildOfType(
-            "function_declaration_left",
-            elmFunction,
-          ),
-        )
-        .find((declaration) => {
-          if (declaration && declaration.firstNamedChild) {
-            return functionName === declaration.firstNamedChild.text;
-          }
-        });
-
-      if (!ret) {
-        for (const elmFunction of functions) {
-          const pattern = TreeUtils.findFirstNamedChildOfType(
-            "pattern",
-            elmFunction,
-          );
-          if (pattern) {
-            ret =
-              pattern
-                .descendantsOfType("lower_pattern")
-                .find((a) => functionName === a.text) ?? undefined;
-
-            if (ret) {
-              break;
-            }
-          }
-        }
-      }
-      return ret;
-    }
-  }
-
-  public static findPort(tree: Tree, portName: string): SyntaxNode | undefined {
-    return TreeUtils.findAllNamedChildrenOfType(
-      "port_annotation",
-      tree.rootNode,
-    )?.find(
-      (node) =>
-        node.children.length > 1 &&
-        node.children[1].type === "lower_case_identifier" &&
-        node.children[1].text === portName,
-    );
-  }
-
   public static findOperator(
     sourceFile: ISourceFile,
     operatorName: string,
@@ -313,21 +237,6 @@ export class TreeUtils {
     return tree.rootNode.childForFieldName("moduleDeclaration") ?? undefined;
   }
 
-  public static findTypeAliasDeclaration(
-    tree: Tree,
-    typeAliasName: string,
-  ): SyntaxNode | undefined {
-    const typeAliases = this.findAllTypeAliasDeclarations(tree);
-    if (typeAliases) {
-      return typeAliases.find(
-        (a) =>
-          a.children.length > 2 &&
-          a.children[2].type === "upper_case_identifier" &&
-          a.children[2].text === typeAliasName,
-      );
-    }
-  }
-
   public static findAllTopLevelFunctionDeclarations(
     tree: Tree,
   ): SyntaxNode[] | undefined {
@@ -337,17 +246,6 @@ export class TreeUtils {
     return result.length === 0 ? undefined : result;
   }
 
-  public static findAllTopLevelFunctionDeclarationsWithoutTypeAnnotation(
-    tree: Tree,
-  ): SyntaxNode[] | undefined {
-    const result = tree.rootNode.children.filter(
-      (a) =>
-        a.type === "value_declaration" &&
-        a.previousNamedSibling?.type !== "type_annotation",
-    );
-    return result.length === 0 ? undefined : result;
-  }
-
   public static findAllTypeOrTypeAliasCalls(
     tree: Tree,
   ): SyntaxNode[] | undefined {
@@ -407,34 +305,6 @@ export class TreeUtils {
     return this.findAllNamedChildrenOfType("type_declaration", tree.rootNode);
   }
 
-  public static findAllTypeAliasDeclarations(
-    tree: Tree,
-  ): SyntaxNode[] | undefined {
-    return this.findAllNamedChildrenOfType(
-      "type_alias_declaration",
-      tree.rootNode,
-    );
-  }
-
-  public static findTypeAliasTypeVariable(
-    nodeAtPosition: SyntaxNode,
-    nodeAtPositionText: string,
-  ): SyntaxNode | undefined {
-    const parentTypeAlias = this.findParentOfType(
-      "type_alias_declaration",
-      nodeAtPosition,
-    );
-
-    if (parentTypeAlias) {
-      const lowerTypeNames = TreeUtils.findAllNamedChildrenOfType(
-        "lower_type_name",
-        parentTypeAlias,
-      );
-
-      return lowerTypeNames?.find((t) => t.text === nodeAtPositionText);
-    }
-  }
-
   /**
    * @deprecated Should not be used due to performance. Use bindings instead
    */
@@ -587,59 +457,6 @@ export class TreeUtils {
     }
   }
 
-  public static getTypeAliasOfCase(
-    type: SyntaxNode | undefined,
-    sourceFile: ISourceFile,
-    program: IProgram,
-  ): SyntaxNode | undefined {
-    if (type) {
-      const definitionNode = program
-        .getTypeChecker()
-        .findDefinition(type, sourceFile).symbol;
-
-      if (definitionNode) {
-        const definitionTree = program
-          .getForest()
-          .getByUri(definitionNode.node.tree.uri);
-
-        let aliasNode;
-        if (definitionNode.type === "FunctionParameter") {
-          aliasNode = TreeUtils.getTypeOrTypeAliasOfFunctionParameter(
-            definitionNode.node,
-          );
-        } else if (definitionNode.type === "Function") {
-          aliasNode = TreeUtils.getReturnTypeOrTypeAliasOfFunctionDefinition(
-            definitionNode.node,
-          );
-        } else if (definitionNode.type === "FieldType") {
-          aliasNode = TreeUtils.findFirstNamedChildOfType(
-            "type_expression",
-            definitionNode.node,
-          );
-        } else if (definitionNode.type === "TypeAlias") {
-          return definitionNode.node;
-        }
-
-        if (aliasNode && definitionTree) {
-          const childNode = TreeUtils.descendantsOfType(
-            aliasNode,
-            "upper_case_identifier",
-          );
-
-          if (childNode.length > 0) {
-            const typeNode = program
-              .getTypeChecker()
-              .findDefinition(childNode[0], definitionTree).symbol;
-
-            if (typeNode) {
-              return typeNode.node;
-            }
-          }
-        }
-      }
-    }
-  }
-
   public static getTypeAliasOfRecord(
     node: SyntaxNode | undefined,
     sourceFile: ISourceFile,


Test patch:
diff --git a/test/codeActionTests/codeActionTestBase.ts b/test/codeActionTests/codeActionTestBase.ts
index ade3811..36196ff 100644
--- a/test/codeActionTests/codeActionTestBase.ts
+++ b/test/codeActionTests/codeActionTestBase.ts
@@ -113,12 +113,11 @@ export async function testCodeAction(
     .resolve(DiagnosticsProvider)
     .forceElmLsDiagnosticsUpdate(sourceFile, program);
 
-  const range = { start: result.position, end: result.position };
   const codeActions =
     codeActionProvider.handleCodeAction({
       program,
       sourceFile,
-      range,
+      range: result.range,
       textDocument: { uri: testUri },
       context: {
         diagnostics: [
@@ -129,7 +128,7 @@ export async function testCodeAction(
             .createDiagnostics(sourceFile, program)
             .map(convertToCompilerDiagnostic),
         ]
-          .filter((diag) => Utils.rangeOverlaps(diag.range, range))
+          .filter((diag) => Utils.rangeOverlaps(diag.range, result.range))
           .map(convertFromCompilerDiagnostic),
       },
     }) ?? [];
@@ -165,7 +164,9 @@ export async function testCodeAction(
       const edits = changesToApply[URI.file(path.join(srcUri, uri)).toString()];
       if (edits) {
         expect(
-          applyEditsToSource(stripCommentLines(result.sources[uri]), edits),
+          trimTrailingWhitespace(
+            applyEditsToSource(stripCommentLines(result.sources[uri]), edits),
+          ),
         ).toEqual(source);
       }
     });
diff --git a/test/codeActionTests/extractFunctionCodeAction.test.ts b/test/codeActionTests/extractFunctionCodeAction.test.ts
new file mode 100644
index 0000000..89297d7
--- /dev/null
+++ b/test/codeActionTests/extractFunctionCodeAction.test.ts
@@ -0,0 +1,345 @@
+import { testCodeAction } from "./codeActionTestBase";
+
+describe("extract function code action", () => {
+  it("should extract function and compute parameters", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            case field1 of
+          --^
+                Just { prop1, prop2 } ->
+                    prop1 + prop2 + val + field2
+
+                Nothing ->
+                    field2 + val2
+                               --^
+
+        Nothing ->
+            str + val2
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            newFunction field1 val field2
+
+        Nothing ->
+            str + val2
+
+
+newFunction : Maybe { a | prop1 : number, prop2 : number } -> Maybe { b | field1 : Maybe { a | prop1 : number, prop2 : number }, field2 : unknown, field3 : c } -> unknown -> unknown
+newFunction field1 val field2 =
+    case field1 of
+        Just { prop1, prop2 } ->
+            prop1 + prop2 + val + field2
+
+        Nothing ->
+            field2 + val2
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to top level" }],
+      expectedSource,
+    );
+  });
+
+  it("should extract function - without inner destructured parameters", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            (\\{ prop1, prop2 } ->
+          --^
+                prop1 + prop2 + val + field2)
+            field1
+                --^
+
+        Nothing ->
+            str + val2
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            newFunction val field2 field1
+
+        Nothing ->
+            str + val2
+
+
+newFunction : Maybe { a | field1 : { b | prop1 : number, prop2 : number }, field2 : unknown, field3 : c } -> unknown -> { b | prop1 : number, prop2 : number } -> unknown
+newFunction val field2 field1 =
+    (\\{ prop1, prop2 } ->
+        prop1 + prop2 + val + field2)
+    field1
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to top level" }],
+      expectedSource,
+    );
+  });
+
+  it("should extract function - without external record field parameters", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+type alias Field1 =
+    { prop1: Int, prop2: Int }
+
+foo : Maybe { field1: Field1, field2: Int, field3: Int } -> Int -> Int
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            (\\{ prop1, prop2 } ->
+          --^
+                prop1 + prop2 + val + field2)
+            field1
+                --^
+
+        Nothing ->
+            str + val2
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+type alias Field1 =
+    { prop1: Int, prop2: Int }
+
+foo : Maybe { field1: Field1, field2: Int, field3: Int } -> Int -> Int
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            newFunction val field2 field1
+
+        Nothing ->
+            str + val2
+
+
+newFunction : Maybe { field1 : Field1, field2 : Int, field3 : Int } -> Int -> Field1 -> unknown
+newFunction val field2 field1 =
+    (\\{ prop1, prop2 } ->
+        prop1 + prop2 + val + field2)
+    field1
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to top level" }],
+      expectedSource,
+    );
+  });
+
+  it("should extract function - without parameter from another file", async () => {
+    const source = `
+--@ Foo.elm
+module Foo exposing (..)
+
+val2 = 
+    0
+
+--@ Test.elm
+module Test exposing (..)
+
+import Foo exposing (..)
+
+foo val =
+    val 
+  --^
+        + val2 
+        + Foo.val2
+                --^
+  
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+import Foo exposing (..)
+
+foo val =
+    newFunction val
+
+
+newFunction : number -> number
+newFunction val =
+    val 
+        + val2 
+        + Foo.val2
+
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to top level" }],
+      expectedSource,
+    );
+  });
+
+  it("should extract function to enclosing let - no parameters", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            let
+                field4 = 
+                    0
+            in
+            case field1 of
+          --^
+                Just { prop1, prop2 } ->
+                    prop1 + prop2 + val + field2 + field4
+
+                Nothing ->
+                    field2 + val2
+                               --^
+
+        Nothing ->
+            str + val2
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    case val of
+        Just { field1, field2, field3 } ->
+            let
+                field4 = 
+                    0
+
+
+                newFunction = 
+                    case field1 of
+                        Just { prop1, prop2 } ->
+                            prop1 + prop2 + val + field2 + field4
+
+                        Nothing ->
+                            field2 + val2
+            in
+            newFunction
+
+        Nothing ->
+            str + val2
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to enclosing let" }],
+      expectedSource,
+    );
+  });
+
+  it("should extract function to enclosing let - some parameters", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    let
+        field4 = 
+            0
+    in
+    case val of
+        Just { field1, field2, field3 } ->
+            case field1 of
+          --^
+                Just { prop1, prop2 } ->
+                    prop1 + prop2 + val + field2 + field4
+
+                Nothing ->
+                    field2 + val2
+                               --^
+
+        Nothing ->
+            str + val2
+`;
+
+    const expectedSource = `
+--@ Test.elm
+module Test exposing (..)
+
+val2 = 
+    0
+
+foo val str =
+    let
+        field4 = 
+            0
+
+
+        newFunction field1 field2 = 
+            case field1 of
+                Just { prop1, prop2 } ->
+                    prop1 + prop2 + val + field2 + field4
+
+                Nothing ->
+                    field2 + val2
+    in
+    case val of
+        Just { field1, field2, field3 } ->
+            newFunction field1 field2
+
+        Nothing ->
+            str + val2
+`;
+
+    await testCodeAction(
+      source,
+      [{ title: "Extract function to enclosing let" }],
+      expectedSource,
+    );
+  });
+});
diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts
index 2c6dd4b..3281f14 100644
--- a/test/completionProvider.test.ts
+++ b/test/completionProvider.test.ts
@@ -1737,4 +1737,60 @@ testFunction =
 
     await testCompletions(source, ["field"]);
   });
+
+  it("Destructed record function parameter", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+type alias Model =
+  { prop1: String
+  , prop2: Int
+  }
+
+testFunction : Model -> a
+testFunction { prop1, prop2 } =
+    pr{-caret-}
+`;
+
+    await testCompletions(source, ["prop1", "prop2"], "partialMatch");
+  });
+
+  it("port completion", async () => {
+    const source = `
+--@ Test.elm
+port module Test exposing (..)
+
+x = 
+    f{-caret-}
+
+port foo : String -> Cmd msg
+
+port fbar : (String -> msg) -> Sub msg
+`;
+
+    await testCompletions(source, ["foo", "fbar"], "partialMatch");
+  });
+
+  it("Completions for record fields", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+type alias Model =
+  { prop1: String
+  , prop2: Int
+  }
+
+view : Model -> String
+view model =
+    m{-caret-}
+`;
+
+    await testCompletions(
+      source,
+      ["model.prop1", "model.prop2"],
+      "partialMatch",
+    );
+  });
 });
diff --git a/test/diagnosticTests/elmDiagnostics.test.ts b/test/diagnosticTests/elmDiagnostics.test.ts
index 7f1b2e7..7f0590b 100644
--- a/test/diagnosticTests/elmDiagnostics.test.ts
+++ b/test/diagnosticTests/elmDiagnostics.test.ts
@@ -127,11 +127,11 @@ describe("test elm diagnostics", () => {
 
     let nodeAtPosition: SyntaxNode = undefined!;
 
-    if ("position" in result) {
+    if ("range" in result) {
       const rootNode = program.getSourceFile(testUri)!.tree.rootNode;
       nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
         rootNode,
-        result.position,
+        result.range.start,
       );
     }
 
diff --git a/test/renameProvider.test.ts b/test/renameProvider.test.ts
index 84c25ea..df66fe9 100644
--- a/test/renameProvider.test.ts
+++ b/test/renameProvider.test.ts
@@ -58,7 +58,7 @@ describe("renameProvider", () => {
     const renameRange = renameProvider.onPrepareRenameRequest({
       program,
       sourceFile,
-      position: result.position,
+      position: result.range.start,
       textDocument: { uri: testUri },
     });
 
@@ -90,7 +90,7 @@ describe("renameProvider", () => {
     const renameEdit = renameProvider.onRenameRequest({
       program,
       sourceFile,
-      position: result.position,
+      position: result.range.start,
       textDocument: { uri: testUri },
       newName,
     });
diff --git a/test/typeInference.test.ts b/test/typeInference.test.ts
index 5695778..aa614ca 100644
--- a/test/typeInference.test.ts
+++ b/test/typeInference.test.ts
@@ -67,7 +67,7 @@ describe("test type inference", () => {
 
     const nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
       sourceFile.tree.rootNode,
-      result.position,
+      result.range.start,
     );
 
     const declaration = TreeUtils.findParentOfType(
diff --git a/test/utils/sourceParser.ts b/test/utils/sourceParser.ts
index 52a0405..42d8442 100644
--- a/test/utils/sourceParser.ts
+++ b/test/utils/sourceParser.ts
@@ -1,8 +1,6 @@
-import { Position } from "vscode-languageserver";
+import { Position, Range } from "vscode-languageserver";
 
-export function getCaretPositionFromSource(
-  source: string,
-): {
+export function getCaretPositionFromSource(source: string): {
   position: Position;
   newSources: { [K: string]: string };
   fileWithCaret: string;
@@ -159,27 +157,35 @@ export function getInvokeAndTargetPositionFromSource(source: string): TestType {
 
 export function getTargetPositionFromSource(
   source: string,
-): { position: Position; sources: { [K: string]: string } } | undefined {
+): { range: Range; sources: { [K: string]: string } } | undefined {
   const sources = getSourceFiles(source);
 
-  let position: Position | undefined;
+  let startPosition: Position | undefined;
+  let endPosition: Position | undefined;
 
   for (const fileName in sources) {
     sources[fileName].split("\n").forEach((s, line) => {
       const invokeCharacter = s.search(/--(\^)/);
 
       if (invokeCharacter >= 0) {
-        position = {
-          line: line - 1,
-          character: invokeCharacter + 2,
-        };
+        if (!startPosition) {
+          startPosition = {
+            line: line - 1,
+            character: invokeCharacter + 2,
+          };
+        } else if (!endPosition) {
+          endPosition = {
+            line: line - 2,
+            character: invokeCharacter + 2,
+          };
+        }
       }
     });
   }
 
-  if (position) {
+  if (startPosition) {
     return {
-      position,
+      range: { start: startPosition, end: endPosition ?? startPosition },
       sources,
     };
   }