diff --git a/boot.py b/boot.py
index 56ae078db..c4a791485 100644
--- a/boot.py
+++ b/boot.py
@@ -69,6 +69,8 @@
 from .plugin.references import LspSymbolReferencesCommand
 from .plugin.rename import LspHideRenameButtonsCommand
 from .plugin.rename import LspSymbolRenameCommand
+from .plugin.rename_file import LspRenameFileCommand
+from .plugin.rename_file import RenameFileCommand
 from .plugin.save_command import LspSaveAllCommand
 from .plugin.save_command import LspSaveCommand
 from .plugin.selection_range import LspExpandSelectionCommand
@@ -146,6 +148,8 @@
     "LspSymbolImplementationCommand",
     "LspSymbolReferencesCommand",
     "LspSymbolRenameCommand",
+    "LspRenameFileCommand",
+    "RenameFileCommand",
     "LspSymbolTypeDefinitionCommand",
     "LspToggleCodeLensesCommand",
     "LspToggleHoverPopupsCommand",
@@ -261,6 +265,10 @@ def on_pre_close(self, view: sublime.View) -> None:
                     tup[1](None)
                     break
 
+    def on_window_command(self, window: sublime.Window, command_name: str, args: dict) -> tuple[str, dict] | None:
+        if command_name == "rename_path":
+            return ("rename_file", args)
+
     def on_post_window_command(self, window: sublime.Window, command_name: str, args: dict[str, Any] | None) -> None:
         if command_name == "show_panel":
             wm = windows.lookup(window)
diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py
index 69a440f72..f47b62907 100644
--- a/plugin/core/protocol.py
+++ b/plugin/core/protocol.py
@@ -6009,6 +6009,10 @@ def colorPresentation(cls, params: ColorPresentationParams, view: sublime.View)
     def willSaveWaitUntil(cls, params: WillSaveTextDocumentParams, view: sublime.View) -> Request:
         return Request("textDocument/willSaveWaitUntil", params, view)
 
+    @classmethod
+    def willRenameFiles(cls, params: RenameFilesParams) -> Request:
+        return Request("workspace/willRenameFiles", params)
+
     @classmethod
     def documentSymbols(cls, params: DocumentSymbolParams, view: sublime.View) -> Request:
         return Request("textDocument/documentSymbol", params, view, progress=True)
@@ -6201,6 +6205,10 @@ def didSave(cls, params: DidSaveTextDocumentParams) -> Notification:
     def didClose(cls, params: DidCloseTextDocumentParams) -> Notification:
         return Notification("textDocument/didClose", params)
 
+    @classmethod
+    def didRenameFiles(cls, params: RenameFilesParams) -> Notification:
+        return Notification("workspace/didRenameFiles", params)
+
     @classmethod
     def didChangeConfiguration(cls, params: DidChangeConfigurationParams) -> Notification:
         return Notification("workspace/didChangeConfiguration", params)
diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py
index ccb30b429..04e7fa41f 100644
--- a/plugin/core/sessions.py
+++ b/plugin/core/sessions.py
@@ -493,6 +493,11 @@ def get_initialize_params(variables: dict[str, str], workspace_folders: list[Wor
         "codeLens": {
             "refreshSupport": True
         },
+        "fileOperations": {
+            "dynamicRegistration": True,
+            "willRename": True,
+            "didRename": True
+        },
         "inlayHint": {
             "refreshSupport": True
         },
diff --git a/plugin/core/types.py b/plugin/core/types.py
index 46d7efa7b..e35bb76f5 100644
--- a/plugin/core/types.py
+++ b/plugin/core/types.py
@@ -2,6 +2,9 @@
 from .collections import DottedDict
 from .file_watcher import FileWatcherEventType
 from .logging import debug, set_debug_logging
+from .protocol import FileOperationFilter
+from .protocol import FileOperationPatternKind
+from .protocol import FileOperationRegistrationOptions
 from .protocol import TextDocumentSyncKind
 from .url import filename_to_uri
 from .url import parse_uri
@@ -10,6 +13,7 @@
 from wcmatch.glob import BRACE
 from wcmatch.glob import globmatch
 from wcmatch.glob import GLOBSTAR
+from wcmatch.glob import IGNORECASE
 import contextlib
 import fnmatch
 import os
@@ -440,6 +444,34 @@ def matches(self, view: sublime.View) -> bool:
         return any(f(view) for f in self.filters) if self.filters else True
 
 
+def match_file_operation_filters(
+    file_operation_options: FileOperationRegistrationOptions, path: str, view: sublime.View | None
+) -> bool:
+    def matches(file_operation_filter: FileOperationFilter) -> bool:
+        pattern = file_operation_filter.get('pattern')
+        scheme = file_operation_filter.get('scheme')
+        if scheme and view:
+            uri = view.settings().get("lsp_uri")
+            if isinstance(uri, str) and parse_uri(uri)[0] != scheme:
+                return False
+        if pattern:
+            matches = pattern.get('matches')
+            if matches == FileOperationPatternKind.File and os.path.isdir(path):
+                return False
+            if matches == FileOperationPatternKind.Folder and os.path.isfile(path):
+                return False
+            options = pattern.get('options', {})
+            flags = GLOBSTAR | BRACE
+            if options.get('ignoreCase', False):
+                flags |= IGNORECASE
+            if not globmatch(path, pattern['glob'], flags=flags):
+                return False
+        return True
+
+    filters = file_operation_options.get('filters')
+    return any(matches(_filter) for _filter in filters) if filters else True
+
+
 # method -> (capability dotted path, optional registration dotted path)
 # these are the EXCEPTIONS. The general rule is: method foo/bar --> (barProvider, barProvider.id)
 _METHOD_TO_CAPABILITY_EXCEPTIONS: dict[str, tuple[str, str | None]] = {
diff --git a/plugin/rename_file.py b/plugin/rename_file.py
new file mode 100644
index 000000000..a73447207
--- /dev/null
+++ b/plugin/rename_file.py
@@ -0,0 +1,129 @@
+from __future__ import annotations
+
+from .core.types import match_file_operation_filters
+from .core.open import open_file_uri
+from .core.protocol import Notification, RenameFilesParams, Request, WorkspaceEdit
+from .core.registry import LspWindowCommand
+from pathlib import Path
+from urllib.parse import urljoin
+import os
+import sublime
+import functools
+
+
+# It is bad that this command is named RenameFileCommand, same as the command in Default/rename.py
+# ST has a bug that prevents the RenameFileCommand to be override in on_window_command:
+# https://github.com/sublimehq/sublime_text/issues/2234
+# So naming this command "RenameFileCommand" is one BAD way to override the rename behavior.
+class RenameFileCommand(LspWindowCommand):
+    def is_enabled(self):
+        return True
+
+    def run(self, paths: list[str] | None = None) -> None:
+        old_path = paths[0] if paths else None
+        path_name = Path(old_path or "").name
+        view = self.window.active_view()
+        if path_name == "" and view:
+            path_name = Path(view.file_name() or "").name
+        v = self.window.show_input_panel(
+            "(LSP) New Name:",
+            path_name,
+            functools.partial(self.on_done, old_path),
+            None,
+            None)
+        v.sel().clear()
+        name, _ext = os.path.splitext(path_name)
+        v.sel().add(sublime.Region(0, len(name)))
+
+    def on_done(self, old_path: str | None, new_name: str) -> None:
+        if new_name:
+            self.window.run_command('lsp_rename_file', {
+                "new_name": new_name,
+                "old_path": old_path
+            })
+
+
+class LspRenameFileCommand(LspWindowCommand):
+    capability = 'workspace.fileOperations.willRename'
+
+    def is_enabled(self):
+        return True
+
+    def want_event(self) -> bool:
+        return False
+
+    def run(
+        self,
+        new_name: str,  # new_name can be: FILE_NAME.xy OR ./FILE_NAME.xy OR ../../FILE_NAME.xy
+        old_path: str | None = None,
+    ) -> None:
+        session = self.session()
+        view = self.window.active_view()
+        old_path = old_path or view.file_name() if view else None
+        if old_path is None:  # handle renaming buffers
+            if view:
+                view.set_name(new_name)
+            return
+        new_path = os.path.normpath(Path(old_path).parent / new_name)
+        if os.path.exists(new_path):
+            self.window.status_message('Unable to Rename. Already exists')
+            return
+        rename_file_params: RenameFilesParams = {
+            "files": [{
+                "newUri": urljoin("file:", new_path),
+                "oldUri": urljoin("file:", old_path),
+            }]
+        }
+        file_operation_options = session.get_capability('workspace.fileOperations.willRename') if session else None
+        if session and file_operation_options and match_file_operation_filters(file_operation_options, old_path, view):
+            request = Request.willRenameFiles(rename_file_params)
+            session.send_request(
+                request,
+                lambda res: self.handle(res, session.config.name, old_path, new_path, rename_file_params, view)
+            )
+        else:
+            self.rename_path(old_path, new_path)
+            self.notify_did_rename(rename_file_params, new_path, view)
+
+    def handle(self, res: WorkspaceEdit | None, session_name: str,
+               old_path: str, new_path: str, rename_file_params: RenameFilesParams, view: sublime.View | None) -> None:
+        if session := self.session_by_name(session_name):
+            # LSP spec - Apply WorkspaceEdit before the files are renamed
+            if res:
+                session.apply_workspace_edit_async(res, is_refactoring=True)
+            self.rename_path(old_path, new_path)
+            self.notify_did_rename(rename_file_params, new_path, view)
+
+    def rename_path(self, old_path: str, new_path: str) -> None:
+        old_regions: list[sublime.Region] = []
+        if view := self.window.find_open_file(old_path):
+            old_regions = [region for region in view.sel()]
+            view.close()  # LSP spec - send didClose for the old file
+        new_dir = Path(new_path).parent
+        if not os.path.exists(new_dir):
+            os.makedirs(new_dir)
+        isdir = os.path.isdir(old_path)
+        try:
+            os.rename(old_path, new_path)
+        except Exception:
+            sublime.status_message("Unable to rename")
+        if isdir:
+            for v in self.window.views():
+                file_name = v.file_name()
+                if file_name and file_name.startswith(old_path):
+                    v.retarget(file_name.replace(old_path, new_path))
+        if os.path.isfile(new_path):
+            def restore_regions(v: sublime.View | None) -> None:
+                if not v:
+                    return
+                v.sel().clear()
+                v.sel().add_all(old_regions)
+
+            # LSP spec - send didOpen for the new file
+            open_file_uri(self.window, new_path).then(restore_regions)
+
+    def notify_did_rename(self, rename_file_params: RenameFilesParams, path: str, view: sublime.View | None):
+        for s in self.sessions():
+            file_operation_options = s.get_capability('workspace.fileOperations.didRename')
+            if file_operation_options and match_file_operation_filters(file_operation_options, path, view):
+                s.send_notification(Notification.didRenameFiles(rename_file_params))
diff --git a/stubs/wcmatch/glob.pyi b/stubs/wcmatch/glob.pyi
index 9d5a7a6f6..0cc199577 100644
--- a/stubs/wcmatch/glob.pyi
+++ b/stubs/wcmatch/glob.pyi
@@ -2,6 +2,7 @@ from typing import Any, Optional
 
 BRACE: int = ...
 GLOBSTAR: int = ...
+IGNORECASE: int = ...
 
 
 def globmatch(