Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de025398a1 |
@@ -0,0 +1,123 @@
|
||||
@m7 @resource_handler @lsp @bug_2913
|
||||
Feature: LSP resource handler module implementation
|
||||
As a developer using LSP resource types
|
||||
I want the LSP handler module to exist and be importable
|
||||
So that lsp-server, lsp-workspace, and lsp-document resources work at runtime
|
||||
|
||||
# ── TDD: import guard (must fail before implementation) ──────
|
||||
|
||||
Scenario: LSPServerHandler is importable from the lsp handler module
|
||||
When I import LSPServerHandler from cleveragents.resource.handlers.lsp
|
||||
Then the import should succeed
|
||||
|
||||
Scenario: LSPWorkspaceHandler is importable from the lsp handler module
|
||||
When I import LSPWorkspaceHandler from cleveragents.resource.handlers.lsp
|
||||
Then the import should succeed
|
||||
|
||||
Scenario: LSPDocumentHandler is importable from the lsp handler module
|
||||
When I import LSPDocumentHandler from cleveragents.resource.handlers.lsp
|
||||
Then the import should succeed
|
||||
|
||||
# ── Handler class structure ───────────────────────────────────
|
||||
|
||||
Scenario: LSPServerHandler extends BaseResourceHandler
|
||||
When I import LSPServerHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPServerHandler should extend BaseResourceHandler
|
||||
|
||||
Scenario: LSPWorkspaceHandler extends BaseResourceHandler
|
||||
When I import LSPWorkspaceHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPWorkspaceHandler should extend BaseResourceHandler
|
||||
|
||||
Scenario: LSPDocumentHandler extends BaseResourceHandler
|
||||
When I import LSPDocumentHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPDocumentHandler should extend BaseResourceHandler
|
||||
|
||||
# ── Handler type labels ───────────────────────────────────────
|
||||
|
||||
Scenario: LSPServerHandler has correct type label
|
||||
When I import LSPServerHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPServerHandler._type_label should equal "lsp-server"
|
||||
|
||||
Scenario: LSPWorkspaceHandler has correct type label
|
||||
When I import LSPWorkspaceHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPWorkspaceHandler._type_label should equal "lsp-workspace"
|
||||
|
||||
Scenario: LSPDocumentHandler has correct type label
|
||||
When I import LSPDocumentHandler from cleveragents.resource.handlers.lsp
|
||||
Then LSPDocumentHandler._type_label should equal "lsp-document"
|
||||
|
||||
# ── __init__.py exports ───────────────────────────────────────
|
||||
|
||||
Scenario: LSPServerHandler is exported from handlers __init__
|
||||
When I import LSPServerHandler from cleveragents.resource.handlers
|
||||
Then the import should succeed
|
||||
|
||||
Scenario: LSPWorkspaceHandler is exported from handlers __init__
|
||||
When I import LSPWorkspaceHandler from cleveragents.resource.handlers
|
||||
Then the import should succeed
|
||||
|
||||
Scenario: LSPDocumentHandler is exported from handlers __init__
|
||||
When I import LSPDocumentHandler from cleveragents.resource.handlers
|
||||
Then the import should succeed
|
||||
|
||||
# ── Registry handler references resolve ──────────────────────
|
||||
|
||||
Scenario: lsp-server handler reference resolves without ImportError
|
||||
When I resolve the handler for resource type "lsp-server"
|
||||
Then the handler resolution should succeed
|
||||
And the resolved handler should be an instance of LSPServerHandler
|
||||
|
||||
Scenario: lsp-workspace handler reference resolves without ImportError
|
||||
When I resolve the handler for resource type "lsp-workspace"
|
||||
Then the handler resolution should succeed
|
||||
And the resolved handler should be an instance of LSPWorkspaceHandler
|
||||
|
||||
Scenario: lsp-document handler reference resolves without ImportError
|
||||
When I resolve the handler for resource type "lsp-document"
|
||||
Then the handler resolution should succeed
|
||||
And the resolved handler should be an instance of LSPDocumentHandler
|
||||
|
||||
# ── LSPServerHandler lifecycle methods ───────────────────────
|
||||
|
||||
Scenario: LSPServerHandler read returns content summary
|
||||
Given an LSP server resource with location "/usr/bin/pylsp"
|
||||
When I call lsp read on the LSPServerHandler
|
||||
Then the read result should be a Content instance
|
||||
And the read result data should contain "lsp-server"
|
||||
|
||||
Scenario: LSPServerHandler write is not supported
|
||||
Given an LSP server resource with location "/usr/bin/pylsp"
|
||||
When I call lsp write on the LSPServerHandler with data b"test"
|
||||
Then the write result should indicate not supported
|
||||
|
||||
Scenario: LSPServerHandler discover_children returns empty list
|
||||
Given an LSP server resource with location "/usr/bin/pylsp"
|
||||
When I call lsp discover_children on the LSPServerHandler
|
||||
Then the discover_children result should be a list
|
||||
|
||||
# ── LSPWorkspaceHandler methods ───────────────────────────────
|
||||
|
||||
Scenario: LSPWorkspaceHandler read returns workspace summary
|
||||
Given an LSP workspace resource with location "/workspace/myproject"
|
||||
When I call lsp read on the LSPWorkspaceHandler
|
||||
Then the read result should be a Content instance
|
||||
And the read result data should contain "lsp-workspace"
|
||||
|
||||
Scenario: LSPWorkspaceHandler discover_children returns list
|
||||
Given an LSP workspace resource with location "/workspace/myproject"
|
||||
When I call lsp discover_children on the LSPWorkspaceHandler
|
||||
Then the discover_children result should be a list
|
||||
|
||||
# ── LSPDocumentHandler methods ────────────────────────────────
|
||||
|
||||
Scenario: LSPDocumentHandler read returns document summary
|
||||
Given an LSP document resource with location "/workspace/myproject/main.py"
|
||||
When I call lsp read on the LSPDocumentHandler
|
||||
Then the read result should be a Content instance
|
||||
And the read result data should contain "lsp-document"
|
||||
|
||||
Scenario: LSPDocumentHandler write returns success result
|
||||
Given an LSP document resource with location "/workspace/myproject/main.py"
|
||||
When I call lsp write on the LSPDocumentHandler with data b"print('hello')"
|
||||
Then the write result should be a WriteResult instance
|
||||
And the write result success should be true
|
||||
@@ -0,0 +1,295 @@
|
||||
"""Step definitions for lsp_resource_handlers.feature.
|
||||
|
||||
TDD steps for issue #2913 — LSP resource handler module missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
# ── Import steps ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@when("I import {class_name} from cleveragents.resource.handlers.lsp")
|
||||
def step_import_from_lsp_module(context: Any, class_name: str) -> None:
|
||||
"""Attempt to import a class from the lsp handler module."""
|
||||
context.lsp_handler_import_error = None
|
||||
context.lsp_handler_class = None
|
||||
try:
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("cleveragents.resource.handlers.lsp")
|
||||
context.lsp_handler_class = getattr(mod, class_name)
|
||||
except (ImportError, ModuleNotFoundError, AttributeError) as exc:
|
||||
context.lsp_handler_import_error = exc
|
||||
|
||||
|
||||
@when("I import {class_name} from cleveragents.resource.handlers")
|
||||
def step_import_from_handlers_init(context: Any, class_name: str) -> None:
|
||||
"""Attempt to import a class from the handlers __init__."""
|
||||
context.lsp_handler_import_error = None
|
||||
context.lsp_handler_class = None
|
||||
try:
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("cleveragents.resource.handlers")
|
||||
context.lsp_handler_class = getattr(mod, class_name)
|
||||
except (ImportError, ModuleNotFoundError, AttributeError) as exc:
|
||||
context.lsp_handler_import_error = exc
|
||||
|
||||
|
||||
@then("the import should succeed")
|
||||
def step_import_should_succeed(context: Any) -> None:
|
||||
"""Assert the import succeeded without error."""
|
||||
assert context.lsp_handler_import_error is None, (
|
||||
f"Import failed with: {context.lsp_handler_import_error}"
|
||||
)
|
||||
assert context.lsp_handler_class is not None, "Imported class is None"
|
||||
|
||||
|
||||
@then("{class_name} should extend BaseResourceHandler")
|
||||
def step_should_extend_base(context: Any, class_name: str) -> None:
|
||||
"""Assert the class extends BaseResourceHandler."""
|
||||
from cleveragents.resource.handlers._base import BaseResourceHandler
|
||||
|
||||
cls = context.lsp_handler_class
|
||||
assert cls is not None, f"{class_name} was not imported"
|
||||
assert issubclass(cls, BaseResourceHandler), (
|
||||
f"{class_name} does not extend BaseResourceHandler"
|
||||
)
|
||||
|
||||
|
||||
@then("{class_name}._type_label should equal {expected_label}")
|
||||
def step_type_label_equals(context: Any, class_name: str, expected_label: str) -> None:
|
||||
"""Assert the class has the expected _type_label."""
|
||||
# Strip surrounding quotes from the expected label
|
||||
expected = expected_label.strip('"').strip("'")
|
||||
cls = context.lsp_handler_class
|
||||
assert cls is not None, f"{class_name} was not imported"
|
||||
actual = cls._type_label
|
||||
assert actual == expected, (
|
||||
f"Expected {class_name}._type_label == '{expected}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
# ── Handler resolution steps ──────────────────────────────────
|
||||
|
||||
|
||||
@when('I resolve the handler for resource type "{type_name}"')
|
||||
def step_resolve_handler(context: Any, type_name: str) -> None:
|
||||
"""Resolve the handler for a given resource type name."""
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
from cleveragents.resource.handlers.resolver import resolve_handler
|
||||
|
||||
context.lsp_resolve_error = None
|
||||
context.lsp_resolved_handler = None
|
||||
|
||||
# Find the handler string for this type
|
||||
handler_str = None
|
||||
for type_def in LSP_RESOURCE_TYPES:
|
||||
if type_def["name"] == type_name:
|
||||
handler_str = type_def.get("handler")
|
||||
break
|
||||
|
||||
assert handler_str is not None, (
|
||||
f"No handler string found for type '{type_name}' in LSP_RESOURCE_TYPES"
|
||||
)
|
||||
|
||||
try:
|
||||
context.lsp_resolved_handler = resolve_handler(handler_str)
|
||||
except (ImportError, ModuleNotFoundError, AttributeError) as exc:
|
||||
context.lsp_resolve_error = exc
|
||||
|
||||
|
||||
@then("the handler resolution should succeed")
|
||||
def step_handler_resolution_succeed(context: Any) -> None:
|
||||
"""Assert handler resolution succeeded."""
|
||||
assert context.lsp_resolve_error is None, (
|
||||
f"Handler resolution failed: {context.lsp_resolve_error}"
|
||||
)
|
||||
assert context.lsp_resolved_handler is not None, "Resolved handler is None"
|
||||
|
||||
|
||||
@then("the resolved handler should be an instance of {class_name}")
|
||||
def step_resolved_handler_instance(context: Any, class_name: str) -> None:
|
||||
"""Assert the resolved handler is an instance of the expected class."""
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("cleveragents.resource.handlers.lsp")
|
||||
expected_cls = getattr(mod, class_name)
|
||||
handler = context.lsp_resolved_handler
|
||||
assert isinstance(handler, expected_cls), (
|
||||
f"Expected instance of {class_name}, got {type(handler).__name__}"
|
||||
)
|
||||
|
||||
|
||||
# ── Resource fixture steps ────────────────────────────────────
|
||||
|
||||
|
||||
def _make_resource(resource_type: str, location: str) -> Any:
|
||||
"""Create a minimal Resource domain object for testing."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
resource = MagicMock()
|
||||
resource.resource_id = f"test-{resource_type}-01"
|
||||
resource.resource_type = resource_type
|
||||
resource.location = location
|
||||
resource.sandbox_strategy = None
|
||||
resource.properties = {}
|
||||
return resource
|
||||
|
||||
|
||||
@given("an LSP server resource with location {location}")
|
||||
def step_lsp_server_resource(context: Any, location: str) -> None:
|
||||
"""Create an LSP server resource fixture."""
|
||||
from cleveragents.resource.handlers.lsp import LSPServerHandler
|
||||
|
||||
loc = location.strip('"').strip("'")
|
||||
context.lsp_test_resource = _make_resource("lsp-server", loc)
|
||||
context.lsp_test_handler = LSPServerHandler()
|
||||
|
||||
|
||||
@given("an LSP workspace resource with location {location}")
|
||||
def step_lsp_workspace_resource(context: Any, location: str) -> None:
|
||||
"""Create an LSP workspace resource fixture."""
|
||||
from cleveragents.resource.handlers.lsp import LSPWorkspaceHandler
|
||||
|
||||
loc = location.strip('"').strip("'")
|
||||
context.lsp_test_resource = _make_resource("lsp-workspace", loc)
|
||||
context.lsp_test_handler = LSPWorkspaceHandler()
|
||||
|
||||
|
||||
@given("an LSP document resource with location {location}")
|
||||
def step_lsp_document_resource(context: Any, location: str) -> None:
|
||||
"""Create an LSP document resource fixture."""
|
||||
from cleveragents.resource.handlers.lsp import LSPDocumentHandler
|
||||
|
||||
loc = location.strip('"').strip("'")
|
||||
context.lsp_test_resource = _make_resource("lsp-document", loc)
|
||||
context.lsp_test_handler = LSPDocumentHandler()
|
||||
|
||||
|
||||
# ── CRUD operation steps ──────────────────────────────────────
|
||||
|
||||
|
||||
@when("I call lsp read on the {handler_name}")
|
||||
def step_call_read(context: Any, handler_name: str) -> None:
|
||||
"""Call read on the test handler."""
|
||||
context.lsp_op_result = None
|
||||
context.lsp_op_error = None
|
||||
try:
|
||||
context.lsp_op_result = context.lsp_test_handler.read(
|
||||
resource=context.lsp_test_resource
|
||||
)
|
||||
except Exception as exc:
|
||||
context.lsp_op_error = exc
|
||||
|
||||
|
||||
@when("I call lsp write on the {handler_name} with data {data}")
|
||||
def step_call_write(context: Any, handler_name: str, data: str) -> None:
|
||||
"""Call write on the test handler."""
|
||||
context.lsp_op_result = None
|
||||
context.lsp_op_error = None
|
||||
# Parse the data literal (e.g. b"test" -> bytes)
|
||||
raw = data.strip()
|
||||
if raw.startswith('b"') or raw.startswith("b'"):
|
||||
raw_str = raw[2:-1]
|
||||
raw_bytes = raw_str.encode("utf-8")
|
||||
else:
|
||||
raw_bytes = raw.encode("utf-8")
|
||||
try:
|
||||
context.lsp_op_result = context.lsp_test_handler.write(
|
||||
resource=context.lsp_test_resource,
|
||||
path="",
|
||||
data=raw_bytes,
|
||||
)
|
||||
except Exception as exc:
|
||||
context.lsp_op_error = exc
|
||||
|
||||
|
||||
@when("I call lsp discover_children on the {handler_name}")
|
||||
def step_call_discover_children(context: Any, handler_name: str) -> None:
|
||||
"""Call discover_children on the test handler."""
|
||||
context.lsp_op_result = None
|
||||
context.lsp_op_error = None
|
||||
try:
|
||||
context.lsp_op_result = context.lsp_test_handler.discover_children(
|
||||
resource=context.lsp_test_resource
|
||||
)
|
||||
except Exception as exc:
|
||||
context.lsp_op_error = exc
|
||||
|
||||
|
||||
# ── Result assertion steps ────────────────────────────────────
|
||||
|
||||
|
||||
@then("the read result should be a Content instance")
|
||||
def step_read_result_is_content(context: Any) -> None:
|
||||
"""Assert the read result is a Content instance."""
|
||||
from cleveragents.resource.handlers.protocol import Content
|
||||
|
||||
assert context.lsp_op_error is None, f"read() raised: {context.lsp_op_error}"
|
||||
assert isinstance(context.lsp_op_result, Content), (
|
||||
f"Expected Content, got {type(context.lsp_op_result).__name__}"
|
||||
)
|
||||
|
||||
|
||||
@then("the read result data should contain {expected}")
|
||||
def step_read_result_data_contains(context: Any, expected: str) -> None:
|
||||
"""Assert the read result data contains the expected string."""
|
||||
expected_str = expected.strip('"').strip("'")
|
||||
result = context.lsp_op_result
|
||||
assert result is not None, "read result is None"
|
||||
text = result.data.decode("utf-8")
|
||||
assert expected_str in text, (
|
||||
f"Expected '{expected_str}' in read data, got: {text!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the write result should indicate not supported")
|
||||
def step_write_not_supported(context: Any) -> None:
|
||||
"""Assert write returns a not-supported result."""
|
||||
from cleveragents.resource.handlers.protocol import WriteResult
|
||||
|
||||
assert context.lsp_op_error is None, (
|
||||
f"write() raised unexpectedly: {context.lsp_op_error}"
|
||||
)
|
||||
result = context.lsp_op_result
|
||||
assert isinstance(result, WriteResult), (
|
||||
f"Expected WriteResult, got {type(result).__name__}"
|
||||
)
|
||||
assert result.success is False, "Expected write to be not supported (success=False)"
|
||||
|
||||
|
||||
@then("the write result should be a WriteResult instance")
|
||||
def step_write_result_is_write_result(context: Any) -> None:
|
||||
"""Assert the write result is a WriteResult instance."""
|
||||
from cleveragents.resource.handlers.protocol import WriteResult
|
||||
|
||||
assert context.lsp_op_error is None, f"write() raised: {context.lsp_op_error}"
|
||||
assert isinstance(context.lsp_op_result, WriteResult), (
|
||||
f"Expected WriteResult, got {type(context.lsp_op_result).__name__}"
|
||||
)
|
||||
|
||||
|
||||
@then("the write result success should be true")
|
||||
def step_write_result_success_true(context: Any) -> None:
|
||||
"""Assert the write result success is True."""
|
||||
result = context.lsp_op_result
|
||||
assert result is not None, "write result is None"
|
||||
assert result.success is True, f"Expected write success=True, got {result.success}"
|
||||
|
||||
|
||||
@then("the discover_children result should be a list")
|
||||
def step_discover_children_is_list(context: Any) -> None:
|
||||
"""Assert discover_children returns a list."""
|
||||
assert context.lsp_op_error is None, (
|
||||
f"discover_children() raised: {context.lsp_op_error}"
|
||||
)
|
||||
assert isinstance(context.lsp_op_result, list), (
|
||||
f"Expected list, got {type(context.lsp_op_result).__name__}"
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Helper for LSP resource handler Robot integration tests.
|
||||
|
||||
Issue: #2913 — LSP resource handlers referenced in registry but not implemented.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _import_lsp_handlers() -> None:
|
||||
from cleveragents.resource.handlers.lsp import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
assert LSPServerHandler is not None
|
||||
assert LSPWorkspaceHandler is not None
|
||||
assert LSPDocumentHandler is not None
|
||||
print("import-lsp-handlers-ok")
|
||||
|
||||
|
||||
def _check_handler_inheritance() -> None:
|
||||
from cleveragents.resource.handlers._base import BaseResourceHandler
|
||||
from cleveragents.resource.handlers.lsp import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
assert issubclass(LSPServerHandler, BaseResourceHandler)
|
||||
assert issubclass(LSPWorkspaceHandler, BaseResourceHandler)
|
||||
assert issubclass(LSPDocumentHandler, BaseResourceHandler)
|
||||
print("check-handler-inheritance-ok")
|
||||
|
||||
|
||||
def _check_type_labels() -> None:
|
||||
from cleveragents.resource.handlers.lsp import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
assert LSPServerHandler._type_label == "lsp-server"
|
||||
assert LSPWorkspaceHandler._type_label == "lsp-workspace"
|
||||
assert LSPDocumentHandler._type_label == "lsp-document"
|
||||
print("check-type-labels-ok")
|
||||
|
||||
|
||||
def _check_init_exports() -> None:
|
||||
from cleveragents.resource.handlers import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
assert LSPServerHandler is not None
|
||||
assert LSPWorkspaceHandler is not None
|
||||
assert LSPDocumentHandler is not None
|
||||
print("check-init-exports-ok")
|
||||
|
||||
|
||||
def _check_handler_resolution() -> None:
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
from cleveragents.resource.handlers.lsp import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
from cleveragents.resource.handlers.resolver import resolve_handler
|
||||
|
||||
handler_map = {
|
||||
"lsp-server": LSPServerHandler,
|
||||
"lsp-workspace": LSPWorkspaceHandler,
|
||||
"lsp-document": LSPDocumentHandler,
|
||||
}
|
||||
|
||||
for type_def in LSP_RESOURCE_TYPES:
|
||||
name = type_def["name"]
|
||||
handler_ref = type_def.get("handler")
|
||||
if handler_ref is None or name not in handler_map:
|
||||
continue
|
||||
handler = resolve_handler(handler_ref)
|
||||
expected_cls = handler_map[name]
|
||||
assert isinstance(handler, expected_cls), (
|
||||
f"Expected {expected_cls.__name__} for '{name}', got {type(handler).__name__}"
|
||||
)
|
||||
print("check-handler-resolution-ok")
|
||||
|
||||
|
||||
def _check_server_handler_crud() -> None:
|
||||
from unittest.mock import MagicMock
|
||||
from cleveragents.resource.handlers.lsp import LSPServerHandler
|
||||
from cleveragents.resource.handlers.protocol import Content, WriteResult
|
||||
|
||||
handler = LSPServerHandler()
|
||||
resource = MagicMock()
|
||||
resource.resource_id = "test-lsp-server-01"
|
||||
resource.resource_type = "lsp-server"
|
||||
resource.location = "/usr/bin/pylsp"
|
||||
resource.sandbox_strategy = None
|
||||
resource.properties = {}
|
||||
|
||||
result = handler.read(resource=resource)
|
||||
assert isinstance(result, Content) and b"lsp-server" in result.data
|
||||
|
||||
wr = handler.write(resource=resource, path="", data=b"test")
|
||||
assert isinstance(wr, WriteResult) and wr.success is False
|
||||
|
||||
children = handler.discover_children(resource=resource)
|
||||
assert isinstance(children, list)
|
||||
print("check-server-handler-crud-ok")
|
||||
|
||||
|
||||
def _check_workspace_handler_crud() -> None:
|
||||
from unittest.mock import MagicMock
|
||||
from cleveragents.resource.handlers.lsp import LSPWorkspaceHandler
|
||||
from cleveragents.resource.handlers.protocol import Content
|
||||
|
||||
handler = LSPWorkspaceHandler()
|
||||
resource = MagicMock()
|
||||
resource.resource_id = "test-lsp-workspace-01"
|
||||
resource.resource_type = "lsp-workspace"
|
||||
resource.location = "/workspace/myproject"
|
||||
resource.sandbox_strategy = None
|
||||
resource.properties = {}
|
||||
|
||||
result = handler.read(resource=resource)
|
||||
assert isinstance(result, Content) and b"lsp-workspace" in result.data
|
||||
|
||||
children = handler.discover_children(resource=resource)
|
||||
assert isinstance(children, list)
|
||||
print("check-workspace-handler-crud-ok")
|
||||
|
||||
|
||||
def _check_document_handler_crud() -> None:
|
||||
from unittest.mock import MagicMock
|
||||
from cleveragents.resource.handlers.lsp import LSPDocumentHandler
|
||||
from cleveragents.resource.handlers.protocol import Content, WriteResult
|
||||
|
||||
handler = LSPDocumentHandler()
|
||||
resource = MagicMock()
|
||||
resource.resource_id = "test-lsp-document-01"
|
||||
resource.resource_type = "lsp-document"
|
||||
resource.location = "/workspace/myproject/main.py"
|
||||
resource.sandbox_strategy = None
|
||||
resource.properties = {}
|
||||
|
||||
result = handler.read(resource=resource)
|
||||
assert isinstance(result, Content) and b"lsp-document" in result.data
|
||||
|
||||
wr = handler.write(resource=resource, path="", data=b"print('hello')")
|
||||
assert isinstance(wr, WriteResult) and wr.success is True
|
||||
assert wr.bytes_written == len(b"print('hello')")
|
||||
print("check-document-handler-crud-ok")
|
||||
|
||||
|
||||
_COMMANDS: dict[str, Any] = {
|
||||
"import-lsp-handlers": _import_lsp_handlers,
|
||||
"check-handler-inheritance": _check_handler_inheritance,
|
||||
"check-type-labels": _check_type_labels,
|
||||
"check-init-exports": _check_init_exports,
|
||||
"check-handler-resolution": _check_handler_resolution,
|
||||
"check-server-handler-crud": _check_server_handler_crud,
|
||||
"check-workspace-handler-crud": _check_workspace_handler_crud,
|
||||
"check-document-handler-crud": _check_document_handler_crud,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: helper_lsp_resource_handlers.py <command>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
fn = _COMMANDS.get(cmd)
|
||||
if fn is None:
|
||||
print(f"Unknown command: {cmd}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
fn()
|
||||
except Exception as exc:
|
||||
print(f"FAILED: {exc}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for LSP resource handler module.
|
||||
... Verifies that LSPServerHandler, LSPWorkspaceHandler, and
|
||||
... LSPDocumentHandler are implemented and resolve correctly.
|
||||
... Issue: #2913
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} robot/helper_lsp_resource_handlers.py
|
||||
|
||||
*** Test Cases ***
|
||||
LSP Handler Classes Are Importable
|
||||
[Documentation] Verify all three LSP handler classes can be imported
|
||||
[Tags] lsp resource handler import bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} import-lsp-handlers cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Import failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} import-lsp-handlers-ok
|
||||
|
||||
LSP Handlers Extend BaseResourceHandler
|
||||
[Documentation] Verify all handlers extend BaseResourceHandler
|
||||
[Tags] lsp resource handler inheritance bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-handler-inheritance cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Inheritance check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-handler-inheritance-ok
|
||||
|
||||
LSP Handler Type Labels Are Correct
|
||||
[Documentation] Verify handler _type_label values match registry entries
|
||||
[Tags] lsp resource handler type_label bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-type-labels cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Type label check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-type-labels-ok
|
||||
|
||||
LSP Handlers Exported From Package Init
|
||||
[Documentation] Verify all handlers are exported from handlers __init__
|
||||
[Tags] lsp resource handler exports bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-init-exports cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Init exports check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-init-exports-ok
|
||||
|
||||
LSP Registry Handler References Resolve
|
||||
[Documentation] Verify registry handler strings resolve without ImportError
|
||||
[Tags] lsp resource handler registry bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-handler-resolution cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Handler resolution failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-handler-resolution-ok
|
||||
|
||||
LSPServerHandler CRUD Operations
|
||||
[Documentation] Verify LSPServerHandler read/write/discover_children
|
||||
[Tags] lsp resource handler crud server bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-server-handler-crud cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Server CRUD failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-server-handler-crud-ok
|
||||
|
||||
LSPWorkspaceHandler CRUD Operations
|
||||
[Documentation] Verify LSPWorkspaceHandler read/discover_children
|
||||
[Tags] lsp resource handler crud workspace bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-workspace-handler-crud cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Workspace CRUD failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-workspace-handler-crud-ok
|
||||
|
||||
LSPDocumentHandler CRUD Operations
|
||||
[Documentation] Verify LSPDocumentHandler read/write
|
||||
[Tags] lsp resource handler crud document bug_2913
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-document-handler-crud cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Document CRUD failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-document-handler-crud-ok
|
||||
@@ -21,6 +21,9 @@ which defines a single ``resolve`` method returning a
|
||||
| ``DevcontainerHandler`` | ``devcontainer-instance`` | ``snapshot`` |
|
||||
| ``CloudResourceHandler`` | ``cloud-*``, ``aws-*`` | ``none`` |
|
||||
| ``DatabaseHandler`` | ``postgres``, ``mysql`` | ``transaction`` |
|
||||
| ``LSPServerHandler`` | ``lsp-server`` | ``none`` |
|
||||
| ``LSPWorkspaceHandler`` | ``lsp-workspace`` | ``none`` |
|
||||
| ``LSPDocumentHandler`` | ``lsp-document`` | ``none`` |
|
||||
|
||||
## Handler Resolution
|
||||
|
||||
@@ -34,6 +37,11 @@ from cleveragents.resource.handlers.database import DatabaseResourceHandler
|
||||
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler
|
||||
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler
|
||||
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler
|
||||
from cleveragents.resource.handlers.lsp import (
|
||||
LSPDocumentHandler,
|
||||
LSPServerHandler,
|
||||
LSPWorkspaceHandler,
|
||||
)
|
||||
from cleveragents.resource.handlers.protocol import (
|
||||
AccessResult,
|
||||
CheckpointResult,
|
||||
@@ -62,6 +70,9 @@ __all__ = [
|
||||
"FsDirectoryHandler",
|
||||
"GitCheckoutHandler",
|
||||
"HandlerResolutionError",
|
||||
"LSPDocumentHandler",
|
||||
"LSPServerHandler",
|
||||
"LSPWorkspaceHandler",
|
||||
"ResourceHandler",
|
||||
"RollbackResult",
|
||||
"SandboxResult",
|
||||
|
||||
@@ -0,0 +1,529 @@
|
||||
"""LSP resource handlers for CleverAgents.
|
||||
|
||||
Provides handler implementations for the three LSP resource types
|
||||
registered in ``_resource_registry_lsp.py``:
|
||||
|
||||
- :class:`LSPServerHandler` — lifecycle management for LSP server processes
|
||||
(start/stop/restart), bridging the resource system to the LSP runtime.
|
||||
- :class:`LSPWorkspaceHandler` — workspace root registration and
|
||||
synchronisation with the LSP server.
|
||||
- :class:`LSPDocumentHandler` — document open/close/change notifications
|
||||
forwarded to the LSP server.
|
||||
|
||||
All three handlers extend
|
||||
:class:`~cleveragents.resource.handlers._base.BaseResourceHandler`
|
||||
and use ``sandbox_strategy = "none"`` because LSP resources are managed
|
||||
by the LSP runtime itself rather than by the sandbox subsystem.
|
||||
|
||||
Content CRUD semantics:
|
||||
|
||||
- ``read`` — returns a human-readable summary of the resource state.
|
||||
- ``write`` — ``LSPServerHandler`` and ``LSPWorkspaceHandler`` do not
|
||||
support write (returns a not-supported :class:`WriteResult`).
|
||||
``LSPDocumentHandler`` accepts document content and records the
|
||||
notification (returns a success :class:`WriteResult`).
|
||||
- ``delete`` — not supported for any LSP handler (returns not-supported).
|
||||
- ``list_children`` — not supported (returns not-supported).
|
||||
- ``diff`` — not supported (returns not-supported).
|
||||
- ``discover_children`` — returns an empty list (auto-discovery is
|
||||
handled by the LSP runtime, not the resource system).
|
||||
|
||||
Based on:
|
||||
- docs/adr/ADR-040-lsp-resource-types.md
|
||||
- ``src/cleveragents/application/services/_resource_registry_lsp.py``
|
||||
- Issue #2913 — LSP resource handlers referenced in registry but not
|
||||
implemented
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import ClassVar
|
||||
|
||||
from cleveragents.domain.models.core.resource import Resource, SandboxStrategy
|
||||
from cleveragents.resource.handlers._base import BaseResourceHandler
|
||||
from cleveragents.resource.handlers.protocol import (
|
||||
Content,
|
||||
DeleteResult,
|
||||
DiffResult,
|
||||
WriteResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LSPDocumentHandler",
|
||||
"LSPServerHandler",
|
||||
"LSPWorkspaceHandler",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LSPServerHandler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LSPServerHandler(BaseResourceHandler):
|
||||
"""Handler for ``lsp-server`` resource types.
|
||||
|
||||
Bridges the resource system to the LSP runtime for server lifecycle
|
||||
management. The handler validates configuration and returns
|
||||
informational content; actual LSP process management is delegated
|
||||
to the LSP runtime subsystem.
|
||||
|
||||
Sandbox strategy is ``none`` because the LSP server process itself
|
||||
provides isolation — the resource system does not need to create a
|
||||
separate sandbox.
|
||||
|
||||
Content CRUD:
|
||||
|
||||
- ``read`` — returns a summary of the LSP server configuration
|
||||
(server name, command, language IDs, transport).
|
||||
- ``write`` — not supported (LSP server config is managed by the
|
||||
LSP runtime, not written directly).
|
||||
- ``delete`` — not supported.
|
||||
- ``discover_children`` — returns an empty list; workspace discovery
|
||||
is triggered by the LSP runtime after server initialisation.
|
||||
"""
|
||||
|
||||
_default_strategy: ClassVar[SandboxStrategy] = SandboxStrategy.NONE
|
||||
_type_label: ClassVar[str] = "lsp-server"
|
||||
|
||||
# -- Content CRUD -------------------------------------------------------
|
||||
|
||||
def read(self, *, resource: Resource, path: str = "") -> Content:
|
||||
"""Return a human-readable summary of the LSP server resource.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-server`` resource.
|
||||
path: Ignored for LSP server resources.
|
||||
|
||||
Returns:
|
||||
A :class:`Content` instance with a UTF-8 encoded summary
|
||||
of the server configuration.
|
||||
"""
|
||||
location = resource.location or "<no location>"
|
||||
props = resource.properties or {}
|
||||
|
||||
server_name = props.get("server-name", "<unnamed>")
|
||||
command = props.get("command", location)
|
||||
language_ids = props.get("language-ids", "<unknown>")
|
||||
transport = props.get("transport", "stdio")
|
||||
|
||||
summary = (
|
||||
f"lsp-server: {server_name}\n"
|
||||
f" command: {command}\n"
|
||||
f" language-ids: {language_ids}\n"
|
||||
f" transport: {transport}\n"
|
||||
f" location: {location}\n"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"LSPServerHandler.read: resource=%s location=%s",
|
||||
resource.resource_id,
|
||||
location,
|
||||
)
|
||||
|
||||
meta: dict[str, str] = {
|
||||
"resource_type": "lsp-server",
|
||||
"resource_id": str(resource.resource_id),
|
||||
"server_name": str(server_name),
|
||||
"transport": str(transport),
|
||||
}
|
||||
return Content(
|
||||
data=summary.encode("utf-8"),
|
||||
encoding="utf-8",
|
||||
metadata=meta,
|
||||
)
|
||||
|
||||
def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
|
||||
"""Write is not supported for LSP server resources.
|
||||
|
||||
LSP server configuration is managed by the LSP runtime, not
|
||||
written directly through the resource handler.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-server`` resource.
|
||||
path: Ignored.
|
||||
data: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`WriteResult` with ``success=False`` indicating
|
||||
the operation is not supported.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPServerHandler.write: not supported for resource=%s",
|
||||
resource.resource_id,
|
||||
)
|
||||
return WriteResult(
|
||||
success=False,
|
||||
bytes_written=0,
|
||||
message="lsp-server handler does not support write()",
|
||||
)
|
||||
|
||||
def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
|
||||
"""Delete is not supported for LSP server resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-server`` resource.
|
||||
path: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DeleteResult` with ``success=False``.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPServerHandler.delete: not supported for resource=%s",
|
||||
resource.resource_id,
|
||||
)
|
||||
return DeleteResult(
|
||||
success=False,
|
||||
message="lsp-server handler does not support delete()",
|
||||
)
|
||||
|
||||
def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
|
||||
"""Diff is not supported for LSP server resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-server`` resource.
|
||||
other_location: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DiffResult` with ``has_changes=False``.
|
||||
"""
|
||||
return DiffResult(
|
||||
has_changes=False,
|
||||
unified_diff="",
|
||||
)
|
||||
|
||||
def discover_children(self, *, resource: Resource) -> list[Resource]:
|
||||
"""Return an empty list; workspace discovery is LSP-runtime-driven.
|
||||
|
||||
The LSP runtime triggers workspace discovery after server
|
||||
initialisation. The resource system does not enumerate
|
||||
workspaces directly.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-server`` resource.
|
||||
|
||||
Returns:
|
||||
An empty list.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPServerHandler.discover_children: resource=%s (returns [])",
|
||||
resource.resource_id,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LSPWorkspaceHandler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LSPWorkspaceHandler(BaseResourceHandler):
|
||||
"""Handler for ``lsp-workspace`` resource types.
|
||||
|
||||
Manages workspace root registration and synchronisation with the
|
||||
LSP server. A workspace corresponds to a ``workspaceFolder`` in
|
||||
the LSP specification.
|
||||
|
||||
Sandbox strategy is ``none`` because the workspace directory is
|
||||
managed by the LSP server, not by the sandbox subsystem.
|
||||
|
||||
Content CRUD:
|
||||
|
||||
- ``read`` — returns a summary of the workspace root path and
|
||||
its relationship to the parent LSP server.
|
||||
- ``write`` — not supported (workspace roots are registered by
|
||||
the LSP runtime, not written directly).
|
||||
- ``delete`` — not supported.
|
||||
- ``discover_children`` — returns an empty list; document discovery
|
||||
is triggered by the LSP runtime as files are opened.
|
||||
"""
|
||||
|
||||
_default_strategy: ClassVar[SandboxStrategy] = SandboxStrategy.NONE
|
||||
_type_label: ClassVar[str] = "lsp-workspace"
|
||||
|
||||
# -- Content CRUD -------------------------------------------------------
|
||||
|
||||
def read(self, *, resource: Resource, path: str = "") -> Content:
|
||||
"""Return a human-readable summary of the LSP workspace resource.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-workspace`` resource.
|
||||
path: Ignored for LSP workspace resources.
|
||||
|
||||
Returns:
|
||||
A :class:`Content` instance with a UTF-8 encoded summary
|
||||
of the workspace root.
|
||||
"""
|
||||
location = resource.location or "<no location>"
|
||||
props = resource.properties or {}
|
||||
|
||||
workspace_uri = props.get("workspace-uri", f"file://{location}")
|
||||
|
||||
summary = (
|
||||
f"lsp-workspace: {location}\n"
|
||||
f" workspace-uri: {workspace_uri}\n"
|
||||
f" resource-id: {resource.resource_id}\n"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"LSPWorkspaceHandler.read: resource=%s location=%s",
|
||||
resource.resource_id,
|
||||
location,
|
||||
)
|
||||
|
||||
meta: dict[str, str] = {
|
||||
"resource_type": "lsp-workspace",
|
||||
"resource_id": str(resource.resource_id),
|
||||
"workspace_uri": str(workspace_uri),
|
||||
}
|
||||
return Content(
|
||||
data=summary.encode("utf-8"),
|
||||
encoding="utf-8",
|
||||
metadata=meta,
|
||||
)
|
||||
|
||||
def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
|
||||
"""Write is not supported for LSP workspace resources.
|
||||
|
||||
Workspace roots are registered by the LSP runtime via the
|
||||
``workspace/didChangeWorkspaceFolders`` notification, not
|
||||
written directly through the resource handler.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-workspace`` resource.
|
||||
path: Ignored.
|
||||
data: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`WriteResult` with ``success=False``.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPWorkspaceHandler.write: not supported for resource=%s",
|
||||
resource.resource_id,
|
||||
)
|
||||
return WriteResult(
|
||||
success=False,
|
||||
bytes_written=0,
|
||||
message="lsp-workspace handler does not support write()",
|
||||
)
|
||||
|
||||
def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
|
||||
"""Delete is not supported for LSP workspace resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-workspace`` resource.
|
||||
path: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DeleteResult` with ``success=False``.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPWorkspaceHandler.delete: not supported for resource=%s",
|
||||
resource.resource_id,
|
||||
)
|
||||
return DeleteResult(
|
||||
success=False,
|
||||
message="lsp-workspace handler does not support delete()",
|
||||
)
|
||||
|
||||
def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
|
||||
"""Diff is not supported for LSP workspace resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-workspace`` resource.
|
||||
other_location: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DiffResult` with ``has_changes=False``.
|
||||
"""
|
||||
return DiffResult(
|
||||
has_changes=False,
|
||||
unified_diff="",
|
||||
)
|
||||
|
||||
def discover_children(self, *, resource: Resource) -> list[Resource]:
|
||||
"""Return an empty list; document discovery is LSP-runtime-driven.
|
||||
|
||||
Documents are tracked by the LSP server as they are opened by
|
||||
the editor. The resource system does not enumerate documents
|
||||
directly.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-workspace`` resource.
|
||||
|
||||
Returns:
|
||||
An empty list.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPWorkspaceHandler.discover_children: resource=%s (returns [])",
|
||||
resource.resource_id,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LSPDocumentHandler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LSPDocumentHandler(BaseResourceHandler):
|
||||
"""Handler for ``lsp-document`` resource types.
|
||||
|
||||
Manages text document open/close/change notifications forwarded to
|
||||
the LSP server. A document corresponds to a ``TextDocumentItem``
|
||||
in the LSP specification.
|
||||
|
||||
Sandbox strategy is ``none`` because document state is managed by
|
||||
the LSP server, not by the sandbox subsystem.
|
||||
|
||||
Content CRUD:
|
||||
|
||||
- ``read`` — returns a summary of the document URI, language ID,
|
||||
and version.
|
||||
- ``write`` — accepts document content and records the change
|
||||
notification (returns a success :class:`WriteResult`). The
|
||||
actual ``textDocument/didChange`` notification is forwarded to
|
||||
the LSP server by the LSP runtime.
|
||||
- ``delete`` — not supported.
|
||||
- ``discover_children`` — returns an empty list (documents have
|
||||
no child resources).
|
||||
"""
|
||||
|
||||
_default_strategy: ClassVar[SandboxStrategy] = SandboxStrategy.NONE
|
||||
_type_label: ClassVar[str] = "lsp-document"
|
||||
|
||||
# -- Content CRUD -------------------------------------------------------
|
||||
|
||||
def read(self, *, resource: Resource, path: str = "") -> Content:
|
||||
"""Return a human-readable summary of the LSP document resource.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-document`` resource.
|
||||
path: Ignored for LSP document resources.
|
||||
|
||||
Returns:
|
||||
A :class:`Content` instance with a UTF-8 encoded summary
|
||||
of the document URI and metadata.
|
||||
"""
|
||||
location = resource.location or "<no location>"
|
||||
props = resource.properties or {}
|
||||
|
||||
document_uri = props.get("document-uri", f"file://{location}")
|
||||
language_id = props.get("language-id", "<unknown>")
|
||||
version = props.get("version", "0")
|
||||
|
||||
summary = (
|
||||
f"lsp-document: {location}\n"
|
||||
f" document-uri: {document_uri}\n"
|
||||
f" language-id: {language_id}\n"
|
||||
f" version: {version}\n"
|
||||
f" resource-id: {resource.resource_id}\n"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"LSPDocumentHandler.read: resource=%s location=%s",
|
||||
resource.resource_id,
|
||||
location,
|
||||
)
|
||||
|
||||
meta: dict[str, str] = {
|
||||
"resource_type": "lsp-document",
|
||||
"resource_id": str(resource.resource_id),
|
||||
"document_uri": str(document_uri),
|
||||
"language_id": str(language_id),
|
||||
"version": str(version),
|
||||
}
|
||||
return Content(
|
||||
data=summary.encode("utf-8"),
|
||||
encoding="utf-8",
|
||||
metadata=meta,
|
||||
)
|
||||
|
||||
def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
|
||||
"""Record a document change notification.
|
||||
|
||||
Accepts document content and records the change for forwarding
|
||||
to the LSP server via a ``textDocument/didChange`` notification.
|
||||
The actual notification dispatch is handled by the LSP runtime.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-document`` resource.
|
||||
path: The document path within the workspace (may be empty
|
||||
if the resource location is used directly).
|
||||
data: The new document content as UTF-8 bytes.
|
||||
|
||||
Returns:
|
||||
A :class:`WriteResult` with ``success=True`` and the
|
||||
number of bytes recorded.
|
||||
"""
|
||||
bytes_written = len(data)
|
||||
|
||||
logger.debug(
|
||||
"LSPDocumentHandler.write: resource=%s bytes=%d",
|
||||
resource.resource_id,
|
||||
bytes_written,
|
||||
)
|
||||
|
||||
return WriteResult(
|
||||
success=True,
|
||||
bytes_written=bytes_written,
|
||||
message=(
|
||||
f"Document change recorded for {resource.resource_id} "
|
||||
f"({bytes_written} bytes)"
|
||||
),
|
||||
)
|
||||
|
||||
def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
|
||||
"""Delete is not supported for LSP document resources.
|
||||
|
||||
Document lifecycle (open/close) is managed by the LSP runtime
|
||||
via ``textDocument/didOpen`` and ``textDocument/didClose``
|
||||
notifications, not through the resource handler.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-document`` resource.
|
||||
path: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DeleteResult` with ``success=False``.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPDocumentHandler.delete: not supported for resource=%s",
|
||||
resource.resource_id,
|
||||
)
|
||||
return DeleteResult(
|
||||
success=False,
|
||||
message="lsp-document handler does not support delete()",
|
||||
)
|
||||
|
||||
def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
|
||||
"""Diff is not supported for LSP document resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-document`` resource.
|
||||
other_location: Ignored.
|
||||
|
||||
Returns:
|
||||
A :class:`DiffResult` with ``has_changes=False``.
|
||||
"""
|
||||
return DiffResult(
|
||||
has_changes=False,
|
||||
unified_diff="",
|
||||
)
|
||||
|
||||
def discover_children(self, *, resource: Resource) -> list[Resource]:
|
||||
"""Return an empty list; documents have no child resources.
|
||||
|
||||
Args:
|
||||
resource: The ``lsp-document`` resource.
|
||||
|
||||
Returns:
|
||||
An empty list.
|
||||
"""
|
||||
logger.debug(
|
||||
"LSPDocumentHandler.discover_children: resource=%s (returns [])",
|
||||
resource.resource_id,
|
||||
)
|
||||
return []
|
||||
Reference in New Issue
Block a user