diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 19a4be710..77ba53557 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -124,6 +124,18 @@ // // See: https://github.com/Microsoft/language-server-protocol/issues/213 // "languageId": "python", // + // # TCP mode (off unless tcp_mode or tcp_port are set) + // + // // Set to "host" if the server connects to the editor. Otherwise, LSP will connect to the server. + // "tcp_mode": "", + // + // // Port to connect to. If tcp_mode="host", you likely want to leave this empty so LSP selects a random port. + // // The chosen port can be passed as a server argument using a {port} placeholder. + // "tcp_port": 1234, + // + // // Host to connect to if not localhost + // "tcp_host": "", + // // # Optional settings (key-value pairs): // // // Sent to server once using workspace/didChangeConfiguration notification diff --git a/docs/index.md b/docs/index.md index 039d6c955..a1a323c83 100644 --- a/docs/index.md +++ b/docs/index.md @@ -459,17 +459,35 @@ or in multi-language form: } ``` -* `command` - specify a full paths, add arguments (if not specified then tcp_port must be specified) -* `tcp_port` - if not specified then stdin/out are used else sets the tcpport to connect to (if no command is specified then it is assumed that some process is listing on this port) +Most important: + +* `enabled` - enables a language server (default is disabled) + +Values that determine if a server should be started and queried for a given document: + * `scopes` - add language flavours, eg. `source.js`, `source.jsx`. * `syntaxes` - syntaxes that enable LSP features on a document, eg. `Packages/Babel/JavaScript (Babel).tmLanguage` * `languageId` - identifies the language for a document - see https://microsoft.github.io/language-server-protocol/specification#textdocumentitem * `languages` - group scope, syntax and languageId together for servers that support more than one language -* `enabled` - enables a language server (default is disabled) -* `settings` - per-project settings (equivalent to VS Code's Workspace Settings) + +Settings used to start and configure a language server: + +* `command` - must be on PATH or specify a full path, add arguments (can be empty if starting manually, then TCP transport must be configured) * `env` - dict of environment variables to be injected into the language server's process (eg. PYTHONPATH) +* `settings` - per-project settings (equivalent to VS Code's Workspace Settings) * `initializationOptions` - options to send to the server at startup (rarely used) +The default transport is stdio, but TCP is also supported. +The port number can be inserted into the server's arguments by adding a `{port}` placeholder in `command`. + +**Server-owned port** + +Set `tcp_port` and optionally `tcp_host` if server running on another host. + +**Editor-owned port** (servers based on vscode-languageserver-node): + +Set `tcp_mode` to "host", leave `tcp_port` unset for automatic port selection. +`tcp_port` can be set if eg. debugging a server. You may want to check out the LSP source and extend the `TCP_CONNECT_TIMEOUT`. ## Per-project overrides diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 624b2aac2..511904230 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,6 +1,6 @@ from .types import ClientConfig, ClientStates, Settings from .protocol import Request -from .transports import start_tcp_transport +from .transports import start_tcp_transport, start_tcp_listener, TCPTransport, Transport from .rpc import Client, attach_stdio_client from .process import start_server from .url import filename_to_uri @@ -9,7 +9,7 @@ from .protocol import completion_item_kinds, symbol_kinds try: from typing import Callable, Dict, Any, Optional - assert Callable and Dict and Any and Optional + assert Callable and Dict and Any and Optional and Transport except ImportError: pass @@ -34,10 +34,22 @@ def with_client(client: Client) -> 'Session': session = None if config.binary_args: - process = start_server(config.binary_args, project_path, env, settings.log_stderr) + tcp_port = config.tcp_port + server_args = config.binary_args + + if config.tcp_mode == "host": + socket = start_tcp_listener(tcp_port or 0) + tcp_port = socket.getsockname()[1] + server_args = list(s.replace("{port}", str(tcp_port)) for s in config.binary_args) + + process = start_server(server_args, project_path, env, settings.log_stderr) if process: - if config.tcp_port: - transport = start_tcp_transport(config.tcp_port, config.tcp_host) + if config.tcp_mode == "host": + client_socket, address = socket.accept() + transport = TCPTransport(client_socket) # type: Transport + session = with_client(Client(transport, settings)) + elif tcp_port: + transport = start_tcp_transport(tcp_port, config.tcp_host) if transport: session = with_client(Client(transport, settings)) else: diff --git a/plugin/core/settings.py b/plugin/core/settings.py index 123bef97e..56f3bcb0b 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -180,7 +180,8 @@ def read_client_config(name: str, client_config: 'Dict') -> ClientConfig: client_config.get("initializationOptions", dict()), client_config.get("settings", dict()), client_config.get("env", dict()), - client_config.get("tcp_host", None) + client_config.get("tcp_host", None), + client_config.get("tcp_mode", None) ) @@ -198,5 +199,6 @@ def update_client_config(config: 'ClientConfig', settings: dict) -> 'ClientConfi settings.get("init_options", config.init_options), settings.get("settings", config.settings), settings.get("env", config.env), - settings.get("tcp_host", config.tcp_host) + settings.get("tcp_host", config.tcp_host), + settings.get("tcp_mode", config.tcp_mode) ) diff --git a/plugin/core/transports.py b/plugin/core/transports.py index adb300bb9..44b353ac5 100644 --- a/plugin/core/transports.py +++ b/plugin/core/transports.py @@ -51,6 +51,16 @@ def state_to_string(state: int) -> str: return StateStrings.get(state, ''.format(state)) +def start_tcp_listener(tcp_port: int) -> socket.socket: + sock = socket.socket() + sock.bind(('', tcp_port)) + port = sock.getsockname()[1] + sock.settimeout(TCP_CONNECT_TIMEOUT) + debug('listening on {}:{}'.format('localhost', port)) + sock.listen(1) + return sock + + def start_tcp_transport(port: int, host: 'Optional[str]' = None) -> 'Transport': start_time = time.time() debug('connecting to {}:{}'.format(host or "localhost", port)) diff --git a/plugin/core/types.py b/plugin/core/types.py index 27bb4efc4..af2a0a17b 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -68,14 +68,25 @@ def __init__(self, language_id: str, scopes: 'List[str]', syntaxes: 'List[str]') class ClientConfig(object): - def __init__(self, name: str, binary_args: 'List[str]', tcp_port: 'Optional[int]', scopes: 'List[str]' = [], - syntaxes: 'List[str]' = [], languageId: 'Optional[str]' = None, - languages: 'List[LanguageConfig]' = [], enabled: bool = True, init_options: dict = dict(), - settings: dict = dict(), env: dict = dict(), tcp_host: 'Optional[str]' = None) -> None: + def __init__(self, + name: str, + binary_args: 'List[str]', + tcp_port: 'Optional[int]', + scopes: 'List[str]' = [], + syntaxes: 'List[str]' = [], + languageId: 'Optional[str]' = None, + languages: 'List[LanguageConfig]' = [], + enabled: bool = True, + init_options: dict = dict(), + settings: dict = dict(), + env: dict = dict(), + tcp_host: 'Optional[str]' = None, + tcp_mode: 'Optional[str]' = None) -> None: self.name = name self.binary_args = binary_args self.tcp_port = tcp_port self.tcp_host = tcp_host + self.tcp_mode = tcp_mode if not languages: languages = [LanguageConfig(languageId, scopes, syntaxes)] if languageId else [] self.languages = languages