Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client-hosted tcp connection #751

Merged
merged 2 commits into from
Oct 14, 2019
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
12 changes: 12 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 17 additions & 5 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions plugin/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)


Expand All @@ -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)
)
10 changes: 10 additions & 0 deletions plugin/core/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ def state_to_string(state: int) -> str:
return StateStrings.get(state, '<unknown state: %d>'.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))
Expand Down
19 changes: 15 additions & 4 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down