Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Flake8 extension for Visual Studio Code
# Flake8 extension for Visual Studio Code
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a trailing space at the end of this line that should be removed.

Suggested change
# Flake8 extension for Visual Studio Code
# Flake8 extension for Visual Studio Code

Copilot uses AI. Check for mistakes.

A Visual Studio Code extension with support for the Flake8 linter. The extension ships with `flake8==7.1.1`.
A Visual Studio Code extension with support for the Flake8 linter. The extension ships with `flake8==7.3.0`.

> **Note**: The minimum version of Flake8 this extension supports is 7.0.0. If you are having issues with Flake8, please report it to [this issue tracker](https://github.com/PyCQA/flake8/issues) as this extension is just a wrapper around Flake8.

Expand Down
86 changes: 52 additions & 34 deletions bundled/tool/lsp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ def update_environ_path() -> None:
import lsp_jsonrpc as jsonrpc
import lsp_utils as utils
from lsprotocol import types as lsp
from pygls import server, uris, workspace
from pygls import uris
from pygls.lsp.server import LanguageServer
from pygls.workspace import TextDocument

WORKSPACE_SETTINGS = {}
GLOBAL_SETTINGS = {}
RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"

MAX_WORKERS = 5
LSP_SERVER = server.LanguageServer(
LSP_SERVER = LanguageServer(
name="flake8-server", version="v0.1.0", max_workers=MAX_WORKERS
)

Expand All @@ -95,28 +97,34 @@ def update_environ_path() -> None:
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
"""LSP handler for textDocument/didOpen request."""
document = LSP_SERVER.workspace.get_document(params.text_document.uri)
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
diagnostics: list[lsp.Diagnostic] = _linting_helper(document)
LSP_SERVER.publish_diagnostics(document.uri, diagnostics)
LSP_SERVER.text_document_publish_diagnostics(
lsp.PublishDiagnosticsParams(uri=document.uri, diagnostics=diagnostics)
)


@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
"""LSP handler for textDocument/didSave request."""
document = LSP_SERVER.workspace.get_document(params.text_document.uri)
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
diagnostics: list[lsp.Diagnostic] = _linting_helper(document)
LSP_SERVER.publish_diagnostics(document.uri, diagnostics)
LSP_SERVER.text_document_publish_diagnostics(
lsp.PublishDiagnosticsParams(uri=document.uri, diagnostics=diagnostics)
)


@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
def did_close(params: lsp.DidCloseTextDocumentParams) -> None:
"""LSP handler for textDocument/didClose request."""
document = LSP_SERVER.workspace.get_document(params.text_document.uri)
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
# Publishing empty diagnostics to clear the entries for this file.
LSP_SERVER.publish_diagnostics(document.uri, [])
LSP_SERVER.text_document_publish_diagnostics(
lsp.PublishDiagnosticsParams(uri=document.uri, diagnostics=[])
)


def _is_supported_file(document: workspace.Document) -> bool:
def _is_supported_file(document: TextDocument) -> bool:
"""Checks if the given document is supported by this tool."""
if document.path:
file_path = pathlib.Path(document.path)
Expand All @@ -125,7 +133,7 @@ def _is_supported_file(document: workspace.Document) -> bool:
return False


def _linting_helper(document: workspace.Document) -> list[lsp.Diagnostic]:
def _linting_helper(document: TextDocument) -> list[lsp.Diagnostic]:
try:
if not _is_supported_file(document):
log_always(f"Skipping linting for {document.uri} skipped: not supported")
Expand All @@ -141,9 +149,11 @@ def _linting_helper(document: workspace.Document) -> list[lsp.Diagnostic]:
result.stdout, severity=settings["severity"]
)
except Exception:
LSP_SERVER.show_message_log(
f"Linting failed with error:\r\n{traceback.format_exc()}",
lsp.MessageType.Error,
LSP_SERVER.window_log_message(
lsp.LogMessageParams(
message=f"Linting failed with error:\r\n{traceback.format_exc()}",
type=lsp.MessageType.Error,
)
)
return []

Expand Down Expand Up @@ -224,7 +234,7 @@ class QuickFixSolutions:
def __init__(self):
self._solutions: Dict[
str,
Callable[[workspace.Document, List[lsp.Diagnostic]], List[lsp.CodeAction]],
Callable[[TextDocument, List[lsp.Diagnostic]], List[lsp.CodeAction]],
] = {}

def quick_fix(
Expand All @@ -234,9 +244,7 @@ def quick_fix(
"""Decorator used for registering quick fixes."""

def decorator(
func: Callable[
[workspace.Document, List[lsp.Diagnostic]], List[lsp.CodeAction]
],
func: Callable[[TextDocument, List[lsp.Diagnostic]], List[lsp.CodeAction]],
):
if isinstance(codes, str):
if codes in self._solutions:
Expand All @@ -252,9 +260,7 @@ def decorator(

def solutions(
self, code: str
) -> Optional[
Callable[[workspace.Document, List[lsp.Diagnostic]], List[lsp.CodeAction]]
]:
) -> Optional[Callable[[TextDocument, List[lsp.Diagnostic]], List[lsp.CodeAction]]]:
"""Given a flake8 error code returns a function, if available, that provides
quick fix code actions."""

Expand All @@ -273,7 +279,7 @@ def solutions(
)
def code_action(params: lsp.CodeActionParams) -> List[lsp.CodeAction]:
"""LSP handler for textDocument/codeAction request."""
document = LSP_SERVER.workspace.get_document(params.text_document.uri)
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)

settings = copy.deepcopy(_get_settings_by_document(document))
code_actions = []
Expand Down Expand Up @@ -323,7 +329,7 @@ def code_action(params: lsp.CodeActionParams) -> List[lsp.CodeAction]:
]
)
def fix_format(
_document: workspace.Document, diagnostics: List[lsp.Diagnostic]
_document: TextDocument, diagnostics: List[lsp.Diagnostic]
) -> List[lsp.CodeAction]:
"""Provides quick fixes which involve formatting document."""
return [
Expand All @@ -350,7 +356,7 @@ def _command_quick_fix(


def _create_workspace_edits(
document: workspace.Document, results: Optional[List[lsp.TextEdit]]
document: TextDocument, results: Optional[List[lsp.TextEdit]]
):
return lsp.WorkspaceEdit(
document_changes=[
Expand Down Expand Up @@ -496,7 +502,7 @@ def _update_workspace_settings(settings):
}


def _get_document_key(document: workspace.Document):
def _get_document_key(document: TextDocument):
if WORKSPACE_SETTINGS:
document_workspace = pathlib.Path(document.path)
workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()}
Expand All @@ -512,7 +518,7 @@ def _get_document_key(document: workspace.Document):
return None


def _get_settings_by_document(document: workspace.Document | None):
def _get_settings_by_document(document: TextDocument | None):
# If not document, return first workspace settings
if document is None or document.path is None:
return list(WORKSPACE_SETTINGS.values())[0]
Expand All @@ -539,7 +545,7 @@ def _get_settings_by_document(document: workspace.Document | None):
# *****************************************************
# Internal execution APIs.
# *****************************************************
def get_cwd(settings: Dict[str, Any], document: Optional[workspace.Document]) -> str:
def get_cwd(settings: Dict[str, Any], document: Optional[TextDocument]) -> str:
"""Returns cwd for the given settings and document."""
if settings["cwd"] == "${workspaceFolder}":
return settings["workspaceFS"]
Expand All @@ -553,7 +559,7 @@ def get_cwd(settings: Dict[str, Any], document: Optional[workspace.Document]) ->


def _run_tool_on_document(
document: workspace.Document,
document: TextDocument,
use_stdin: bool = False,
extra_args: Sequence[str] = [],
) -> utils.RunResult | None:
Expand Down Expand Up @@ -752,28 +758,40 @@ def log_to_output(
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
) -> None:
"""Logs messages to Output > Flake8 channel only."""
LSP_SERVER.show_message_log(message, msg_type)
LSP_SERVER.window_log_message(lsp.LogMessageParams(message=message, type=msg_type))


def log_error(message: str) -> None:
"""Logs messages with notification on error."""
LSP_SERVER.show_message_log(message, lsp.MessageType.Error)
LSP_SERVER.window_log_message(
lsp.LogMessageParams(message=message, type=lsp.MessageType.Error)
)
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]:
LSP_SERVER.show_message(message, lsp.MessageType.Error)
LSP_SERVER.window_show_message(
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Error)
)


def log_warning(message: str) -> None:
"""Logs messages with notification on warning."""
LSP_SERVER.show_message_log(message, lsp.MessageType.Warning)
LSP_SERVER.window_log_message(
lsp.LogMessageParams(message=message, type=lsp.MessageType.Warning)
)
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]:
LSP_SERVER.show_message(message, lsp.MessageType.Warning)
LSP_SERVER.window_show_message(
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Warning)
)


def log_always(message: str) -> None:
"""Logs messages with notification."""
LSP_SERVER.show_message_log(message, lsp.MessageType.Info)
LSP_SERVER.window_log_message(
lsp.LogMessageParams(message=message, type=lsp.MessageType.Info)
)
if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]:
LSP_SERVER.show_message(message, lsp.MessageType.Info)
LSP_SERVER.window_show_message(
lsp.ShowMessageParams(message=message, type=lsp.MessageType.Info)
)


# *****************************************************
Expand Down
59 changes: 28 additions & 31 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,50 +1,47 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --generate-hashes ./requirements.in
attrs==25.1.0 \
--hash=sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e \
--hash=sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a
attrs==25.4.0 \
--hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \
--hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373
# via
# cattrs
# lsprotocol
cattrs==24.1.2 \
--hash=sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0 \
--hash=sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85
# pygls
cattrs==25.3.0 \
--hash=sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a \
--hash=sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff
# via
# lsprotocol
# pygls
exceptiongroup==1.2.2 \
--hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \
--hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc
# via cattrs
flake8==7.1.1 \
--hash=sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38 \
--hash=sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213
flake8==7.3.0 \
--hash=sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e \
--hash=sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872
# via -r ./requirements.in
lsprotocol==2023.0.1 \
--hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \
--hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d
lsprotocol==2025.0.0 \
--hash=sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29 \
--hash=sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7
# via pygls
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via flake8
packaging==24.2 \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
packaging==26.0 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529
# via -r ./requirements.in
pycodestyle==2.12.1 \
--hash=sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3 \
--hash=sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521
pycodestyle==2.14.0 \
--hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \
--hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d
# via flake8
pyflakes==3.2.0 \
--hash=sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f \
--hash=sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a
pyflakes==3.4.0 \
--hash=sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58 \
--hash=sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f
# via flake8
pygls==1.3.1 \
--hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \
--hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e
pygls==2.0.1 \
--hash=sha256:2f774a669fbe2ece977d302786f01f9b0c5df7d0204ea0fa371ecb08288d6b86 \
--hash=sha256:d29748042cea5bedc98285eb3e2c0c60bf3fc73786319519001bf72bbe8f36cc
# via -r ./requirements.in
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via cattrs
Loading