diff -ruN -x .git yls/.gitignore yls-new/.gitignore
--- yls/.gitignore	2023-05-06 16:00:27.471104541 +0200
+++ yls-new/.gitignore	2023-05-06 21:27:11.439697971 +0200
@@ -2,3 +2,6 @@
 .ropeproject
 __pycache__
 dist
+
+*.whl
+yls/modules
diff -ruN -x .git yls/poetry.lock yls-new/poetry.lock
--- yls/poetry.lock	2023-05-06 16:00:27.503104232 +0200
+++ yls-new/poetry.lock	2023-05-06 21:28:02.367362953 +0200
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
 [[package]]
 name = "astroid"
diff -ruN -x .git yls/pytest-yls/pytest_yls/plugin.py yls-new/pytest-yls/pytest_yls/plugin.py
--- yls/pytest-yls/pytest_yls/plugin.py	2023-05-06 16:00:27.507104193 +0200
+++ yls-new/pytest-yls/pytest_yls/plugin.py	2023-05-06 21:27:11.443697945 +0200
@@ -10,6 +10,7 @@
 import subprocess
 import time
 from collections import defaultdict
+from difflib import ndiff
 from pathlib import Path
 from threading import Thread
 from typing import Any
@@ -64,6 +65,7 @@
     CALL_TIMEOUT_SECONDS = 2.00
     LANGUAGE_ID = "yara"
     CURSOR_SYMBOL = "<$>"
+    KEYSTROKES_PER_MINUTE = 500
 
     def __init__(
         self,
@@ -157,6 +159,97 @@
         )
         self.opened_file = path
 
+    ###
+    def index_to_point(self, name: str, index: int) -> tuple(int, int):
+        """Provides conversion of index in string to point as (line, col)"""
+        path = self.tmp_path / name
+        if not path.exists():
+            raise ValueError(
+                f'USAGE ERROR: File "{name}" does not exists -> Please open one of prepared files'
+            )
+
+        row: int = 0
+        col: int = 0
+        for ch in path.read_text()[0:index]:
+            col += 1
+            if ch == "\n":
+                col = 0
+                row += 1
+
+        return row, col
+
+    def type_new_content(self, name: str, new_content: str) -> None:
+        """Simulates typing of user"""
+
+        path = self.tmp_path / name
+        if not path.exists():
+            raise ValueError(
+                f'USAGE ERROR: File "{name}" does not exists -> Please open one of prepared files'
+            )
+
+        content = path.read_text()
+
+        new_text: str
+        tmp_content: str
+        start_row: int
+        start_col: int
+        end_row: int
+        end_col: int
+
+        del_char_n = 0
+        for index, diff in enumerate(ndiff(content, new_content)):
+            time.sleep(60 / self.KEYSTROKES_PER_MINUTE)
+            content = path.read_text()
+
+            if diff[0] == "+":
+                new_text = diff[-1]
+                start_row, start_col = self.index_to_point(name, index)
+                end_row = start_row
+                end_col = start_col
+
+                tmp_content = content[:index] + diff[-1] + content[index:]
+                path.write_text(tmp_content)
+            elif diff[0] == "-":
+                new_text = ""
+                index -= del_char_n
+                start_row, start_col = self.index_to_point(name, index)
+                end_row, end_col = self.index_to_point(name, index + 1)
+
+                tmp_content = content[:index] + content[index + 1 :]
+                path.write_text(tmp_content)
+                del_char_n += 1
+            else:
+                continue
+
+            # print(path.read_text())
+
+            self.notify(
+                lsp_types.TEXT_DOCUMENT_DID_CHANGE,
+                lsp_types.DidChangeTextDocumentParams(
+                    text_document=lsp_types.TextDocumentItem(
+                        uri=path.as_uri(),
+                        language_id=Context.LANGUAGE_ID,
+                        version=1,
+                        text=path.read_text(),
+                    ),
+                    content_changes=[
+                        lsp_types.TextDocumentContentChangeEvent_Type1(
+                            range=lsp_types.Range(
+                                lsp_types.Position(start_row, start_col),
+                                lsp_types.Position(end_row, end_col),
+                            ),
+                            text=new_text,
+                        )
+                    ],
+                ),
+            )
+
+        time.sleep(
+            self.notification_timeout_seconds
+        )  # Wait for incoming notifications to avoid broken pipe errors
+
+    ###
+
     def send_request(self, feature: str, params: Any) -> Any:
         return self.client.lsp.send_request(feature, params).result(self.call_timeout_seconds)
 
diff -ruN -x .git yls/tests/integration/test_linting.py yls-new/tests/integration/test_linting.py
--- yls/tests/integration/test_linting.py	2023-05-06 16:00:27.515104117 +0200
+++ yls-new/tests/integration/test_linting.py	2023-05-06 21:27:11.443697945 +0200
@@ -1,8 +1,13 @@
 # type: ignore
 
+
+import os
+
 import lsprotocol.types as lsp_types
 import pytest
 
+from yls import utils
+
 pytestmark = pytest.mark.sleep
 
 
@@ -64,3 +69,232 @@
     params = context.wait_for_notification(lsp_types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
     assert hasattr(params, "diagnostics")
     assert not bool(params.diagnostics)
+
+
+###
+def check_diagnostic_object(exp: dict, real: lsp_types.Diagnostic) -> None:
+    if "range" in exp:
+        assert exp["range"] == real.range
+
+    if "message" in exp:
+        assert exp["message"] == real.message
+
+    if "source" in exp:
+        assert exp["source"] == real.source
+
+    if "severity" in exp:
+        assert exp["severity"] == real.severity
+
+
+@pytest.mark.parametrize(
+    "initial, final, exp_diagnostics",
+    [
+        # Addition of the new rule
+        ("", "rule a { condition: true } ", {"num": 0}),
+        # Missing } at the end of the rule
+        (
+            "",
+            "rule b { condition: true",
+            {
+                "num": 1,
+                "content": [
+                    {
+                        "range": utils.range_from_coords((0, 23), (0, 24)),
+                        "message": "Missing }!",
+                        "source": "Yaramod-v4",
+                    }
+                ],
+            },
+        ),
+        # Rule id collision
+        (
+            "rule a { condition: true } ",
+            "rule a { condition: true } rule a { condition: true } ",
+            {
+                "num": 1,
+                "content": [
+                    {"range": utils.range_from_coords((0, 32), (0, 33)), "source": "Yaramod-v4"}
+                ],
+            },
+        ),
+        # Change of colliding rule id
+        (
+            "rule a { condition: true } rule a { condition: true } ",
+            "rule a { condition: true } rule b { condition: true } ",
+            {"num": 0},
+        ),
+        # String addition
+        (
+            """rule a {
+    strings:
+        $string1 = "test"
+    condition:
+        true
+}
+""",
+            """rule a {
+    strings:
+        $string1 = "test"
+        $string2 = /test/
+    condition:
+        true
+}
+""",
+            {"num": 0},
+        ),
+        # Strings id collision
+        (
+            """rule a {
+    strings:
+        $string1 = "test"
+    condition:
+        true
+}
+""",
+            """rule a {
+    strings:
+        $string1 = "test"
+        $string1 = /test/
+    condition:
+        true
+}
+""",
+            {
+                "num": 1,
+                "content": [
+                    {"range": utils.range_from_coords((3, 8), (3, 16)), "source": "Yaramod-v4"}
+                ],
+            },
+        ),
+    ],
+)
+def test_linting_during_writing_in_one_file(
+    yls_prepare_with_settings, initial: str, final: str, exp_diagnostics: dict
+) -> None:
+    """Tests linting with Yaramod-v4 while user is typing"""
+
+    initial_contents = {"file.yar": initial}
+
+    context = yls_prepare_with_settings(files=initial_contents)
+
+    context.type_new_content(list(initial_contents.keys())[0], final)
+
+    notifications = context.get_nofications(
+        lsp_types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS, True
+    )  # There is no need to wait, because, there is sleep in type_new_content
+
+    params = notifications[len(notifications) - 1]
+    assert hasattr(params, "diagnostics")
+
+    if "num" in exp_diagnostics:
+        assert exp_diagnostics["num"] == len(params.diagnostics)
+
+    if "content" in exp_diagnostics:
+        for index, error in enumerate(exp_diagnostics["content"]):
+            check_diagnostic_object(error, params.diagnostics[index])
+
+
+@pytest.mark.parametrize(
+    "initial, final, exp_diagnostics",
+    [
+        # Addition of the new rule, that is already in included file
+        (
+            {"file.yar": "", "file2.yar": "rule a { condition: true }"},
+            {
+                "file.yar": """
+
+                include "file2.yar"
+                rule a { condition: true  }
+                """
+            },
+            {"file.yar": {"num": 1}, "file2.yar": {"num": 0}},
+        ),
+        # Addition of included file with the same rule
+        (
+            {"file.yar": "", "file2.yar": "rule a { condition: true }"},
+            {
+                "file.yar": """
+
+                rule a { condition: true  }
+
+                include "file2.yar"
+                """
+            },
+            {"file.yar": {"num": 0}, "file2.yar": {"num": 1}},
+        ),
+        # Continuation of previous scenario - user decided to removed the one of the duplicated rules
+        (
+            {
+                "file.yar": """
+
+rule a { condition: true  }
+
+include "file2.yar"
+                """,
+                "file2.yar": "rule a { condition: true }",
+            },
+            {
+                "file.yar": """
+
+
+
+include "file2.yar"
+                """
+            },
+            {"file.yar": {"num": 0}, "file2.yar": {"num": 0}},
+        ),
+        # Addition of already included file
+        (
+            {
+                "file.yar": """
+
+                include "file2.yar"
+
+
+                """,
+                "file2.yar": "rule a { condition: true }",
+            },
+            {
+                "file.yar": """
+
+                include "file2.yar"
+
+                include "file2.yar"
+                """
+            },
+            {"file.yar": {"num": 1}, "file2.yar": {"num": 0}},
+        ),
+    ],
+)
+def test_linting_during_writing_in_multiple_files(
+    yls_prepare_with_settings, initial: dict, final: dict, exp_diagnostics: dict
+) -> None:
+    """Tests linting with Yaramod-v4 while user is typing to file with file includes"""
+
+    context = yls_prepare_with_settings(files=initial)
+
+    for filename in list(final.keys()):
+        context.type_new_content(filename, final[filename])
+
+    notifications = context.get_nofications(lsp_types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS, True)
+
+    for filename in list(exp_diagnostics.keys()):
+        params = None
+        for n in reversed(notifications):
+            assert hasattr(n, "uri")
+            if os.path.basename(n.uri) == filename:
+                params = n
+                break
+
+        assert params is not None
+
+        if filename in exp_diagnostics.keys():
+            if "num" in exp_diagnostics[filename]:
+                assert exp_diagnostics[filename]["num"] == len(params.diagnostics)
+
+            if "content" in exp_diagnostics[filename]:
+                for index, error in enumerate(exp_diagnostics[filename]["content"]):
+                    check_diagnostic_object(error, params.diagnostics[index])
+
+
+###
diff -ruN -x .git yls/yls/completer.py yls-new/yls/completer.py
--- yls/yls/completer.py	2023-05-06 16:00:27.519104077 +0200
+++ yls-new/yls/completer.py	2023-05-06 21:27:11.443697945 +0200
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import logging
+import pathlib
 import re
 from typing import Any
 
@@ -97,8 +98,8 @@
         res += self.complete_word(document, params.position, word)
 
         # Symbols from last yara file completion
-        log.debug("[COMPLETION] Adding last valid yara file")
-        res += self.complete_last_valid_yara_file(document, params, word)
+        log.debug("[COMPLETION] Adding yara file symbols")
+        res += self.complete_from_yara_file(document, params, word)
 
         # Plugin completion
         log.debug("COMPLETION] Adding completion items from plugings")
@@ -108,43 +109,71 @@
 
         return res
 
-    def complete_last_valid_yara_file(
+    ###
+    def complete_from_yara_file(
         self, document: Document, params: lsp_types.CompletionParams, word: str
     ) -> list[lsp_types.CompletionItem]:
-        """Return list of completion items from last valid YaraFile."""
-        if self.ls.last_valid_yara_file is None:
+        """Return list of completion items from YaraSource."""
+
+        if self.ls.yara_source_object is None:
+            return []
+
+        text_doc = self.ls.workspace.get_document(params.text_document.uri)
+        abs_path = str(pathlib.Path(text_doc.path).absolute().resolve())
+
+        file = self.ls.yara_source_object.get_file(abs_path)
+        if file is None:
             return []
 
         if not utils.is_in_yara_section(document, params.position.line, "condition"):
             return []
 
         res = []
+        for rule in file.rules:
+            if rule.id.startswith(word) and rule.get_position().row < params.position.line:
+                res += [
+                    lsp_types.CompletionItem(
+                        label=rule.id,
+                        insert_text=rule.id,
+                        detail=utils.get_text_formatted(rule),
+                        sort_text="zb",
+                    )
+                ]
 
-        # Add rules
-        log.debug("[COMPLETION] Adding symbols from last valid YARA file")
-        res += [
-            lsp_types.CompletionItem(label=rule.name, insert_text=rule.name, sort_text="zb")
-            for rule in self.ls.last_valid_yara_file.rules
-            if rule.name.startswith(word)
-        ]
-
-        rule = self.ls.get_current_rule(
-            params.text_document.uri, params.position, self.ls.last_valid_yara_file
-        )
+        rule = self.ls.ymod4_get_current_rule(params.text_document.uri, params.position)
         if rule is None:
             return res
 
         # Add current rule strings
-        res += [
-            lsp_types.CompletionItem(
-                label=string.identifier, insert_text=string.identifier, sort_text="za"
-            )
-            for string in rule.strings
-            if string.identifier.startswith(word)
-        ]
+        for string in rule.strings:
+            string_id_token = "$" + string.id  # Yaramodv4 is saved without dollar sign
+            if string.id.startswith(word):
+                res += [
+                    lsp_types.CompletionItem(
+                        label=string_id_token,
+                        insert_text=string_id_token,
+                        detail=utils.get_text_formatted(string),
+                        sort_text="za",
+                    )
+                ]
+
+        # Add current rule variables
+        for var in rule.vars:
+            log.debug(var.id)
+            if var.id.startswith(word):
+                res += [
+                    lsp_types.CompletionItem(
+                        label=var.id,
+                        insert_text=var.id,
+                        detail=utils.get_text_formatted(var),
+                        sort_text="za",
+                    )
+                ]
 
         return res
 
+    ###
+
     def complete_word(
         self, document: Document, position: lsp_types.Position, word: str
     ) -> list[lsp_types.CompletionItem]:
diff -ruN -x .git yls/yls/linting.py yls-new/yls/linting.py
--- yls/yls/linting.py	2023-05-06 16:00:27.519104077 +0200
+++ yls-new/yls/linting.py	2023-05-06 21:27:11.447697919 +0200
@@ -4,6 +4,7 @@
 import re
 
 import yaramod
+import yaramodv4
 from lsprotocol.types import Diagnostic
 from lsprotocol.types import DiagnosticSeverity
 from pygls.workspace import Document
@@ -14,13 +15,20 @@
 
 log = logging.getLogger(__name__)
 
+###
+def get_diagnostics(
+    document: Document, yara_source_object: yaramodv4.YaraSource | None = None
+) -> list[Diagnostic]:
 
-def get_diagnostics(document: Document) -> list[Diagnostic]:
     """Return diagnostic information for the given `Document`.
 
     This will perform various linting procedures and return a list of
     `Diagnostic`."""
-    return Linter(document).lint()
+
+    return Linter(document).lint(yara_source_object)
+
+
+###
 
 
 class Linter:
@@ -28,6 +36,10 @@
     SOURCE_YARA = "Yara Python"
     SOURCE_YARAMOD = "Yaramod"
 
+    ###
+    SOURCE_YARAMODV4 = "Yaramod-v4"
+    ###
+
     SCHEMA_JSON_FILENAME = "schema.json"
 
     def __init__(self, document: Document):
@@ -36,14 +48,19 @@
         # Result of the linting
         self.diag: list[Diagnostic] = []
 
-    def lint(self) -> list[Diagnostic]:
+    ###
+    def lint(self, yara_source_object: yaramodv4.YaraSource | None) -> list[Diagnostic]:
+        ###
         """Invoke linting procedure for each linter."""
         diagnostic = []
 
         diagnostic += utils.flatten_list(
             PluginManagerProvider.instance().hook.yls_lint(document=self.document)
         )
-        diagnostic += self.lint_with_yaramod()
+
+        ###
+        diagnostic += self.lint_with_yaramodv4(yara_source_object)
+        ###
 
         return diagnostic
 
@@ -71,3 +88,61 @@
                 return [diagnostic]
 
         return []
+
+    ###
+    def lint_with_yaramodv4(
+        self, yara_source_object: yaramodv4.YaraSource | None
+    ) -> list[Diagnostic]:
+        """Perform linting with Yaramodv4."""
+
+        if yara_source_object is None:
+            return []
+
+        file = yara_source_object.get_file(self.document.path)
+        if file is None:
+            return []
+
+        diagnostics = []
+
+        # Get all syntax errors from the yaramodv4
+        for error in file.syntax_errors:
+            raw_err_range = error.get_range()
+            is_valid, err_range = utils.ymod4_to_lsp_range(raw_err_range)
+            if not is_valid:
+                continue
+
+            error_description = utils.get_error_description(error)
+            message = (
+                f"Syntax error! {error_description}"
+                if not isinstance(error, yaramodv4.MissingTokenError)
+                else f"Missing {error_description}!"
+            )
+
+            diagnostics.append(
+                Diagnostic(
+                    range=err_range,
+                    message=message,
+                    source=self.SOURCE_YARAMODV4,
+                    severity=DiagnosticSeverity.Error,
+                )
+            )
+
+        # Get all semantic errors from the yaramodv4
+        for error in file.semantic_errors:
+            raw_err_range = error.get_range()
+            is_valid, err_range = utils.ymod4_to_lsp_range(raw_err_range)
+            if not is_valid:
+                continue
+
+            diagnostics.append(
+                Diagnostic(
+                    range=err_range,
+                    message=utils.get_error_description(error),
+                    source=self.SOURCE_YARAMODV4,
+                    severity=DiagnosticSeverity.Error,
+                )
+            )
+
+        return diagnostics
+
+    ###
diff -ruN -x .git yls/yls/server.py yls-new/yls/server.py
--- yls/yls/server.py	2023-05-06 16:00:27.519104077 +0200
+++ yls-new/yls/server.py	2023-05-06 21:27:11.447697919 +0200
@@ -2,12 +2,14 @@
 
 import functools
 import logging
+import os
 import pathlib
 import re
 from typing import Any
 
 import lsprotocol.types as lsp_types
 import yaramod
+import yaramodv4
 from lsprotocol.types import INITIALIZED
 from lsprotocol.types import TEXT_DOCUMENT_CODE_ACTION
 from lsprotocol.types import TEXT_DOCUMENT_CODE_LENS
@@ -70,12 +72,13 @@
         # Lint the file when editing the document
         self.did_change_lint = False
 
-        # Cached version of yara file used for completions
-        # NOTE: We need to create dict not only one variable
-        self.last_valid_yara_file: yaramod.YaraFile | None = None
-
         self.register_plugin_commands()
 
+        ###
+        self.yara_source_object: yaramodv4.YaraSource | None = None
+        self.must_be_reparsed: bool = False
+        ###
+
     def register_plugin_commands(self) -> None:
         """Initialize the custom commands from plugins."""
 
@@ -119,6 +122,61 @@
             last_rule = rule
         return last_rule
 
+    ###
+    def update_yara_source(self, doc_uri: str, force: bool = False) -> None:
+        """Edit YaraSource object stored in YaraLanguageServer due to params"""
+
+        # Get Document object
+        text_doc = self.workspace.get_document(doc_uri)
+        abs_path = text_doc.path
+
+        log.debug(f"[YMODV4] Updating internal YaraSource object... (file {abs_path})")
+
+        parser = YaramodProvider.ymodv4_instance()
+
+        # If force param is True or there is not valid YaraSource object, parse document from scratch
+        force_request = self.must_be_reparsed or force
+
+        if (
+            self.yara_source_object is None
+            or not self.yara_source_object.entry_file.name == abs_path
+            or force_request
+        ):
+            self.yara_source_object = parser.parse_string(text_doc.source, abs_path)
+        else:
+            self.yara_source_object = parser.parse_string(text_doc.source, self.yara_source_object)
+
+        self.must_be_reparsed = False
+
+        log.debug("[YMODV4] End of updating internal YaraSource object")
+
+    def ymod4_get_current_rule(
+        self, uri: str, position: lsp_types.Position
+    ) -> yaramodv4.Rule | None:
+        """Get the last defined rule for given cursor position."""
+        doc_path = self.workspace.get_document(uri).path
+
+        log.debug(f"[YMODV4_GET_CURRENT_RULE] {position=}, {doc_path=}")
+
+        if self.yara_source_object is None:
+            self.update_yara_source(uri)
+
+        yara_src = self.yara_source_object
+        if yara_src is None:
+            return None
+
+        file = yara_src.get_file(doc_path)
+        if file is None:
+            return None
+
+        rule = file.get_local_rule_by_row(position.line)
+
+        # log.debug(rule.id)
+
+        return rule
+
+    ###
+
     def get_references(
         self, uri: str, word: str, cursor: lsp_types.Position | None = None
     ) -> list[lsp_types.Location]:
@@ -164,7 +222,7 @@
 
 
 @SERVER.feature(INITIALIZED)
-def initiliazed(_ls: YaraLanguageServer, _params: Any) -> None:
+def initiliazed(_ls: YaraLanguageServer, _params: Any = None) -> None:
     """Connection is initialized."""
     utils.log_command(INITIALIZED)
     log.debug("[INITIALIZED] Connection was established")
@@ -203,51 +261,81 @@
     return await ls.hoverer.hover(params)
 
 
-def lint(
-    ls: YaraLanguageServer,
-    params: lsp_types.DidOpenTextDocumentParams
-    | lsp_types.DidChangeTextDocumentParams
-    | lsp_types.DidSaveTextDocumentParams,
-) -> None:
-    """Lint and publish diagnostics."""
-    # NOTE: DiagnosticRelatedInformation can be used to show the redefinition of a rule/string
-    # Represents a related message and source code location for a diagnostic. This should be
-    # used to point to code locations that cause or are related to a diagnostics, e.g when
-    # duplicating a symbol in a scope.
+###
+def edit_yara_source(ls: YaraLanguageServer, params: lsp_types.DidChangeTextDocumentParams) -> None:
+    """Edit YaraSource object stored in YaraLanguageServer due to params"""
+
+    # If there is no valid YaraSource object, edit cannot be reported
+    if ls.yara_source_object is None:
+        return
 
     text_doc = ls.workspace.get_document(params.text_document.uri)
-    log.info(f'[LINT] Lint request start for file "{text_doc.path}"')
+    abs_path = str(pathlib.Path(text_doc.path).absolute().resolve())
+
+    log.debug(f"[YMODV4] Editing internal YaraSource object (file {abs_path})")
 
-    # Save the last valid yarafile, that can be used as a fallback in subsequent requests
-    yara_file = utils.yaramod_parse_file(text_doc.path)
-    if yara_file is not None:
-        ls.last_valid_yara_file = yara_file
+    # Report all edits, that are in params
+    if ls.yara_source_object.has_file(abs_path):
+        for change in utils.get_content_changes(params):
+            if isinstance(change, lsp_types.TextDocumentContentChangeEvent_Type1):
+                assert isinstance(change, lsp_types.TextDocumentContentChangeEvent_Type1)
+                ls.yara_source_object.edit(
+                    utils.lsp_to_ymod4_range(change.range), change.text, abs_path
+                )
+            elif isinstance(change, lsp_types.TextDocumentContentChangeEvent_Type2):
+                ls.must_be_reparsed = True
 
-    diag = linting.get_diagnostics(text_doc)
+    log.debug("[YMODV4] End of editing")
 
-    ls.publish_diagnostics(text_doc.uri, diag)
+
+def fly_lint(ls: YaraLanguageServer) -> None:
+    """Lint and publish diagnostics"""
+
+    log.debug("[YMODV4] Linting started")
+
+    if ls.yara_source_object is None:
+        return
+
+    for file in ls.yara_source_object.files:
+        text_doc = ls.workspace.get_document(file)
+        diag = linting.get_diagnostics(text_doc, ls.yara_source_object)
+        ls.publish_diagnostics(text_doc.uri, diag)
+
+    log.debug("[YMODV4] Linting ended")
+
+
+###
 
 
 @SERVER.feature(TEXT_DOCUMENT_DID_OPEN)
 def did_open(ls: YaraLanguageServer, params: lsp_types.DidOpenTextDocumentParams) -> None:
     """The document was opened."""
     utils.log_command(TEXT_DOCUMENT_DID_OPEN)
-    lint(ls, params)
+
+    text_doc = ls.workspace.get_document(params.text_document.uri)
+    if ls.yara_source_object is None or not ls.yara_source_object.has_file(text_doc.path):
+        ls.update_yara_source(params.text_document.uri, True)
+
+    fly_lint(ls)
 
 
 @SERVER.feature(TEXT_DOCUMENT_DID_SAVE)
 def did_save(ls: YaraLanguageServer, params: lsp_types.DidSaveTextDocumentParams) -> None:
     """The document was saved."""
     utils.log_command(TEXT_DOCUMENT_DID_SAVE)
-    lint(ls, params)
+    ls.update_yara_source(params.text_document.uri)
+    fly_lint(ls)
 
 
 @SERVER.feature(TEXT_DOCUMENT_DID_CHANGE)
 def did_change(ls: YaraLanguageServer, params: lsp_types.DidChangeTextDocumentParams) -> None:
     """The document was changed."""
     utils.log_command(TEXT_DOCUMENT_DID_CHANGE)
-    if ls.did_change_lint:
-        lint(ls, params)
+
+    edit_yara_source(ls, params)
+    ls.update_yara_source(params.text_document.uri)
+
+    fly_lint(ls)
 
 
 @SERVER.feature(TEXT_DOCUMENT_FORMATTING)
@@ -400,61 +488,139 @@
 #     # NOTE: This needs to find all *.yar files in the workspace and provide information on them
 #     return None
 
+###
+def get_document_symbol_includes(file: yaramodv4.YaraFile) -> list[lsp_types.DocumentSymbol]:
+    """Provide a list of included files in the current document."""
 
-@SERVER.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
-def document_symbol(
-    ls: YaraLanguageServer, params: lsp_types.DocumentSymbolParams
-) -> list[lsp_types.DocumentSymbol]:
-    """Provide a list of symbols in the current document."""
-    utils.log_command(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
-    text_doc = ls.workspace.get_document(params.text_document.uri)
+    res: list[lsp_types.DocumentSymbol] = []
+    for include in file.local_file_includes:
+        include_range = include.get_range()
+
+        is_valid, v_range = utils.ymod4_to_lsp_range(include_range)
+        if not is_valid:
+            continue
+
+        res.append(
+            lsp_types.DocumentSymbol(
+                name=os.path.basename(include.path),
+                kind=lsp_types.SymbolKind.File,
+                range=v_range,
+                selection_range=v_range,
+            )
+        )
+
+    return res
+
+
+def get_document_symbol_imports(file: yaramodv4.YaraFile) -> list[lsp_types.DocumentSymbol]:
+    """Provide a list of imported modules in the current document."""
 
     res: list[lsp_types.DocumentSymbol] = []
-    parsed_yfile = utils.yaramod_parse_file(text_doc.path)
-    if parsed_yfile is None:
-        return []
+    for import_statement in file.local_imports:
+        import_range = import_statement.get_range()
 
-    for rule in utils.yaramod_rules_in_file(parsed_yfile):
+        is_valid, v_range = utils.ymod4_to_lsp_range(import_range)
+        if not is_valid:
+            continue
 
-        # Construct the string information for this rule. This is necessary because yls is
-        # creating the whole document symbol hierarchy instead of flat list
-        rule_strings: list[lsp_types.DocumentSymbol] = []
+        res.append(
+            lsp_types.DocumentSymbol(
+                name=import_statement.module_name,
+                kind=lsp_types.SymbolKind.Module,
+                range=v_range,
+                selection_range=v_range,
+            )
+        )
+
+    return res
+
+
+def get_document_symbol_rules(file: yaramodv4.YaraFile) -> list[lsp_types.DocumentSymbol]:
+    """Provide a list of rules and its children in the current document."""
+
+    res: list[lsp_types.DocumentSymbol] = []
+    for rule in file.local_rules:
+        rule_range = rule.get_range()
+
+        is_valid, symbol_range = utils.ymod4_to_lsp_range(rule_range)
+        if not is_valid:
+            continue
+
+        rule_symbols: list[lsp_types.DocumentSymbol] = []
         for string in rule.strings:
-            rule_strings.append(
+            is_valid, v_range = utils.ymod4_to_lsp_range(string.get_range())
+            if string.is_anonymous() or not is_valid:
+                continue
+
+            rule_symbols.append(
                 lsp_types.DocumentSymbol(
-                    name=string.identifier,
-                    detail=string.text,
+                    name=string.id,
+                    detail=utils.format_string_content(string),
                     kind=lsp_types.SymbolKind.String,
-                    range=utils.range_from_coords(
-                        (string.location.begin.line - 1, string.location.begin.column - 1),
-                        (string.location.end.line, string.location.end.column),
-                    ),
-                    selection_range=utils.range_from_coords(
-                        (string.location.begin.line - 1, string.location.begin.column - 1),
-                        (string.location.end.line, string.location.end.column),
-                    ),
+                    range=v_range,
+                    selection_range=v_range,
+                )
+            )
+
+        for var in rule.vars:
+            val = var.value
+            is_valid, v_range = utils.ymod4_to_lsp_range(var.get_range())
+            if not is_valid:
+                continue
+
+            var_type = "" if val is None else yaramodv4.Expression.type_to_str(val.get_type())
+            rule_symbols.append(
+                lsp_types.DocumentSymbol(
+                    name=var.id,
+                    detail=f"({var_type})",
+                    kind=lsp_types.SymbolKind.Variable,
+                    range=v_range,
+                    selection_range=v_range,
                 )
             )
 
         res.append(
             lsp_types.DocumentSymbol(
-                name=rule.name,
+                name=rule.id,
                 kind=lsp_types.SymbolKind.Class,
-                range=utils.range_from_coords(
-                    (rule.location.begin.line - 1, rule.location.begin.column),
-                    (rule.location.end.line - 1, rule.location.end.column),
-                ),
-                selection_range=utils.range_from_coords(
-                    (rule.location.begin.line - 1, rule.location.begin.column),
-                    (rule.location.begin.line - 1, rule.location.begin.column),
-                ),
-                children=rule_strings,
+                range=symbol_range,
+                selection_range=symbol_range,
+                children=rule_symbols,
             )
         )
 
     return res
 
 
+###
+
+
+@SERVER.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
+def document_symbol(
+    ls: YaraLanguageServer, params: lsp_types.DocumentSymbolParams
+) -> list[lsp_types.DocumentSymbol]:
+    """Provide a list of symbols in the current document."""
+    utils.log_command(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
+
+    ###
+    text_doc = ls.workspace.get_document(params.text_document.uri)
+
+    res: list[lsp_types.DocumentSymbol] = []
+    if ls.yara_source_object is None:
+        return []
+
+    file = ls.yara_source_object.get_file(text_doc.path)
+    if file is None:
+        return []
+
+    res.extend(get_document_symbol_includes(file))
+    res.extend(get_document_symbol_imports(file))
+    res.extend(get_document_symbol_rules(file))
+
+    return res
+    ###
+
+
 @SERVER.feature(TEXT_DOCUMENT_CODE_ACTION)
 def code_action(
     ls: YaraLanguageServer, params: lsp_types.CodeActionParams
diff -ruN -x .git yls/yls/utils.py yls-new/yls/utils.py
--- yls/yls/utils.py	2023-05-06 16:00:27.523104040 +0200
+++ yls-new/yls/utils.py	2023-05-06 21:27:11.447697919 +0200
@@ -16,6 +16,7 @@
 
 import lsprotocol.types as lsp_types
 import yaramod
+import yaramodv4
 from pluggy import PluginManager
 from pygls.workspace import Document
 
@@ -779,3 +780,84 @@
     assert len(config) == 1
 
     return config[0]
+
+
+def lsp_to_ymod4_range(to_be_converted: lsp_types.Range) -> yaramodv4.Range:
+    """Provides conversion from LSP range to Yaramodv4 range"""
+    return yaramodv4.Range(
+        to_be_converted.start.line,
+        to_be_converted.start.character,
+        to_be_converted.end.line,
+        to_be_converted.end.character,
+    )
+
+
+def ymod4_to_lsp_range(ymod_range: yaramodv4.Range) -> tuple[bool, lsp_types.Range]:
+    """Provides conversion from Yaramodv4 range to LSP range with the check of
+    range validity."""
+    try:
+        result = range_from_coords(
+            (ymod_range.start.row, ymod_range.start.col), (ymod_range.end.row, ymod_range.end.col)
+        )
+
+    # yaramodv4 in some unexpected situations may return ranges with greater
+    # coords than 2^31 and in this case ValueError is thrown
+    except ValueError:
+        return False, range_from_coords((0, 0), (0, 0))
+
+    return True, result
+
+
+def format_string_content(string: yaramodv4.String) -> str:
+    """Formats string content due to string type"""
+
+    string_type = string.type
+    if string_type == yaramodv4.StringType.Plain:
+        return f'"{get_string_content(string)}"'
+    elif string_type == yaramodv4.StringType.Regexp:
+        return get_string_content(string)
+    elif string_type == yaramodv4.StringType.Hex:
+        return f"{{ {get_string_content(string)} }}"
+    else:
+        return get_string_content(string)
+
+
+def get_string_content(string: yaramodv4.String) -> str:
+    """Provides safe way how to get string content of from Yaramodv4 string
+    object
+    """
+
+    try:
+        assert isinstance(string.content, str)
+        return string.content
+    except UnicodeDecodeError:
+        return ""
+
+
+def get_error_description(error: yaramodv4.Error) -> str:
+    """Provides safe way how to get error description string"""
+
+    try:
+        assert isinstance(error.description, str)
+        return error.description
+    except UnicodeDecodeError:
+        return ""
+
+
+def get_text_formatted(element: Any) -> str:
+    """Provides safe way how to get formatted text of any Yaramodv4 element"""
+
+    try:
+        assert isinstance(element.get_text_formatted(), str)
+        return str(element.get_text_formatted())
+    except UnicodeDecodeError:
+        return ""
+
+
+def get_content_changes(
+    change_params: lsp_types.DidChangeTextDocumentParams,
+) -> list[lsp_types.TextDocumentContentChangeEvent]:
+    if not hasattr(change_params, "content_changes"):
+        return []
+    else:
+        return change_params.content_changes
diff -ruN -x .git yls/yls/yaramod_provider.py yls-new/yls/yaramod_provider.py
--- yls/yls/yaramod_provider.py	2023-05-06 16:00:27.523104040 +0200
+++ yls-new/yls/yaramod_provider.py	2023-05-06 21:27:11.447697919 +0200
@@ -3,22 +3,43 @@
 from __future__ import annotations
 
 import logging
+import os
 
 import yaramod
+import yaramodv4
 
 from yls.plugin_manager_provider import PluginManagerProvider
 
 log = logging.getLogger(__name__)
 
+MODULE_DIR = "modules"
+
 
 class YaramodProvider:
     """Singleton class providing Yaramod parser object."""
 
     ymod: yaramod.Yaramod | None = None
 
+    ymodv4: yaramodv4.Yaramod | None = None
+
     @classmethod
     def instance(cls) -> yaramod.Yaramod:
         """Return singleton instance."""
         if cls.ymod is None:
             cls.ymod = PluginManagerProvider.instance().hook.create_yaramod_instance()
         return cls.ymod
+
+    @classmethod
+    def ymodv4_instance(cls) -> yaramodv4.Yaramod:
+        """Return singleton instance of yaramodv4 parser."""
+        if cls.ymodv4 is None:
+            cls.ymodv4 = yaramodv4.Yaramod()
+
+            # Setting path with modules
+            cur_dir = os.path.dirname(os.path.realpath(__file__))
+            module_dir_path = os.path.join(cur_dir, MODULE_DIR)
+
+            cls.ymodv4.config.module_dir = module_dir_path
+            cls.ymodv4.load_modules()
+
+        return cls.ymodv4
