diff --git a/README.md b/README.md index ab4bfc8..3ef16b7 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,13 @@ if __name__ == "__main__": The library includes pre-configured clients for popular language servers: -| Language Server | Module Path | Language | -| --------------- | ---------------------------------- | --------------------- | -| Pyright | `lsp_client.clients.pyright` | Python | -| Pyrefly | `lsp_client.clients.pyrefly` | Python | -| Rust Analyzer | `lsp_client.clients.rust_analyzer` | Rust | -| Deno | `lsp_client.clients.deno` | TypeScript/JavaScript | +| Language Server | Module Path | Language | +| ---------------------------- | ------------------------------------ | --------------------- | +| Pyright | `lsp_client.clients.pyright` | Python | +| Pyrefly | `lsp_client.clients.pyrefly` | Python | +| Rust Analyzer | `lsp_client.clients.rust_analyzer` | Rust | +| Deno | `lsp_client.clients.deno` | TypeScript/JavaScript | +| TypeScript Language Server | `lsp_client.clients.typescript` | TypeScript/JavaScript | ## Contributing diff --git a/examples/typescript.py b/examples/typescript.py new file mode 100644 index 0000000..2199286 --- /dev/null +++ b/examples/typescript.py @@ -0,0 +1,44 @@ +# Example: Using TypeScript Language Server for TypeScript code analysis +# +# This example demonstrates how to use the TypeScript Language Server +# to find definition locations in TypeScript code. The typescript-language-server +# provides powerful static analysis for TypeScript and JavaScript projects. + +from __future__ import annotations + +from pathlib import Path + +import anyio + +from lsp_client import Position # noqa: F401 - Used in commented example code +from lsp_client.clients.typescript import TypescriptClient, TypescriptServer + + +async def main(): + # Set up workspace directory + workspace = Path.cwd() + async with TypescriptClient( + server=TypescriptServer(), + workspace=workspace, + ) as client: + # Example: Find definition of TypescriptClient at line 13, column 28 + # This demonstrates the definition lookup capability + print(f"Using language server for: {client.get_language_id()}") + + # In a real scenario, you would provide a TypeScript file path + # refs = await client.request_definition_locations( + # file_path="src/index.ts", + # position=Position(10, 15), + # ) + # + # if not refs: + # print("No definition locations found.") + # return + # + # # Display all definition locations found + # for ref in refs: + # print(f"Definition location found at {ref.uri} - Range: {ref.range}") + + +if __name__ == "__main__": + anyio.run(main) # Run the async example diff --git a/src/lsp_client/clients/__init__.py b/src/lsp_client/clients/__init__.py index 5bacf0e..4341e5e 100644 --- a/src/lsp_client/clients/__init__.py +++ b/src/lsp_client/clients/__init__.py @@ -6,12 +6,14 @@ from .pyrefly import PyreflyClient from .pyright import PyrightClient from .rust_analyzer import RustAnalyzerClient +from .typescript import TypescriptClient clients: Final = ( DenoClient, PyreflyClient, PyrightClient, RustAnalyzerClient, + TypescriptClient, ) PythonClient = PyrightClient diff --git a/src/lsp_client/clients/typescript.py b/src/lsp_client/clients/typescript.py new file mode 100644 index 0000000..ec83cb4 --- /dev/null +++ b/src/lsp_client/clients/typescript.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import shutil +from functools import partial +from subprocess import CalledProcessError +from typing import Any, override + +import anyio +from attrs import define +from loguru import logger + +from lsp_client.capability.request import ( + WithRequestDefinition, + WithRequestDocumentSymbol, + WithRequestHover, + WithRequestImplementation, + WithRequestReferences, + WithRequestTypeDefinition, + WithRequestWorkspaceSymbol, +) +from lsp_client.capability.server_notification import ( + WithReceivePublishDiagnostics, +) +from lsp_client.capability.server_notification.log_message import WithReceiveLogMessage +from lsp_client.client.abc import LSPClient +from lsp_client.server.docker import DockerServer +from lsp_client.server.local import LocalServer +from lsp_client.utils.types import lsp_type + +TypescriptServer = partial( + LocalServer, command=["typescript-language-server", "--stdio"] +) +TypescriptDockerServer = partial(DockerServer, image="docker.io/lspcontainers/tsserver") + + +@define +class TypescriptClient( + LSPClient, + WithRequestHover, + WithRequestDefinition, + WithRequestReferences, + WithRequestImplementation, + WithRequestTypeDefinition, + WithRequestDocumentSymbol, + WithRequestWorkspaceSymbol, + WithReceiveLogMessage, + WithReceivePublishDiagnostics, +): + """ + - Language: TypeScript, JavaScript + - Homepage: https://github.com/typescript-language-server/typescript-language-server + - Doc: https://github.com/typescript-language-server/typescript-language-server#readme + - Github: https://github.com/typescript-language-server/typescript-language-server + - VSCode Extension: Built-in TypeScript support in VS Code + """ + + # Preferences for TypeScript/JavaScript language features + # Reference: https://github.com/typescript-language-server/typescript-language-server#initializationoptions + suggest_complete_function_calls: bool = True + include_automatic_optional_chain_completions: bool = True + include_completions_for_module_exports: bool = True + include_completions_with_insert_text: bool = True + + @override + def get_language_id(self) -> lsp_type.LanguageKind: + return lsp_type.LanguageKind.TypeScript + + @override + def create_initialization_options(self) -> dict[str, Any]: + return { + "preferences": { + "includeCompletionsForModuleExports": self.include_completions_for_module_exports, + "includeAutomaticOptionalChainCompletions": self.include_automatic_optional_chain_completions, + "includeCompletionsWithInsertText": self.include_completions_with_insert_text, + }, + "suggest": { + "completeFunctionCalls": self.suggest_complete_function_calls, + }, + } + + @override + def check_server_compatibility(self, info: lsp_type.ServerInfo | None) -> None: + return + + @override + async def ensure_installed(self) -> None: + if shutil.which("typescript-language-server"): + return + + logger.warning( + "typescript-language-server not found, attempting to install via npm..." + ) + + try: + # typescript-language-server requires the TypeScript compiler as a peer dependency + # Reference: https://github.com/typescript-language-server/typescript-language-server#installing + await anyio.run_process( + ["npm", "install", "-g", "typescript-language-server", "typescript"] + ) + logger.info("Successfully installed typescript-language-server via npm") + return + except CalledProcessError as e: + raise RuntimeError( + "Could not install typescript-language-server and typescript. Please install them manually with 'npm install -g typescript-language-server typescript'. " + "See https://github.com/typescript-language-server/typescript-language-server for more information." + ) from e