Skip to content

Commit

Permalink
Merge c190a69 into 2072a12
Browse files Browse the repository at this point in the history
  • Loading branch information
tomv564 committed Sep 30, 2019
2 parents 2072a12 + c190a69 commit 0d2e843
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 19 deletions.
3 changes: 3 additions & 0 deletions .style.yapf
@@ -0,0 +1,3 @@
[style]
based_on_style = pep8
column_limit = 120
9 changes: 8 additions & 1 deletion docs/features.md
Expand Up @@ -61,7 +61,13 @@ https://stackoverflow.com/questions/16235706/sublime-3-set-key-map-for-function-

## Configuring

Global plugin settings and settings defined at project level are merged together.
### Sublime settings

Add these settings to your Sublime settings, Syntax-specific settings and/or in Project files.

* `lsp_format_on_save` `false` *run the server's formatProvider (if supported) on a document before saving.*

### Package settings (LSP)

* `complete_all_chars` `true` *request completions for all characters, not just trigger characters*
* `only_show_lsp_completions` `false` *disable sublime word completion and snippets from autocomplete lists*
Expand All @@ -88,3 +94,4 @@ Global plugin settings and settings defined at project level are merged together
* `log_payloads` `false` *show full JSON-RPC responses in the console*



10 changes: 7 additions & 3 deletions plugin/core/protocol.py
@@ -1,6 +1,6 @@
try:
from typing import Any, List, Dict, Tuple, Callable, Optional
assert Any and List and Dict and Tuple and Callable and Optional
from typing import Any, List, Dict, Tuple, Callable, Optional, Union
assert Any and List and Dict and Tuple and Callable and Optional and Union
except ImportError:
pass

Expand Down Expand Up @@ -153,6 +153,10 @@ def workspaceSymbol(cls, params: dict) -> 'Request':
def formatting(cls, params: dict) -> 'Request':
return Request("textDocument/formatting", params)

@classmethod
def willSaveWaitUntil(cls, params: dict) -> 'Request':
return Request("textDocument/willSaveWaitUntil", params)

@classmethod
def rangeFormatting(cls, params: dict) -> 'Request':
return Request("textDocument/rangeFormatting", params)
Expand Down Expand Up @@ -188,7 +192,7 @@ def to_payload(self, id: int) -> 'Dict[str, Any]':


class Response:
def __init__(self, request_id: int, result: 'Optional[Dict[str, Any]]') -> None:
def __init__(self, request_id: int, result: 'Optional[Union[Dict[str, Any], List[Any]]]') -> None:
self.request_id = request_id
self.result = result
self.jsonrpc = "2.0"
Expand Down
46 changes: 42 additions & 4 deletions plugin/core/rpc.py
Expand Up @@ -14,9 +14,11 @@
from .logging import debug, exception_log
from .protocol import Request, Notification, Response
from .types import Settings

from threading import Condition
from threading import Lock

TCP_CONNECT_TIMEOUT = 5
DEFAULT_SYNC_REQUEST_TIMEOUT = 1.0

# RequestDict = TypedDict('RequestDict', {'id': 'Union[str,int]', 'method': str, 'params': 'Optional[Any]'})

Expand Down Expand Up @@ -75,14 +77,21 @@ def __init__(self, transport: Transport, settings: Settings) -> None:
self._response_handlers = {} # type: Dict[int, Tuple[Optional[Callable], Optional[Callable[[Any], None]]]]
self._request_handlers = {} # type: Dict[str, Callable]
self._notification_handlers = {} # type: Dict[str, Callable]
self._sync_request_results = {} # type: Dict[int, Optional[Any]]
self._sync_request_lock = Lock()
self._sync_request_cvar = Condition(self._sync_request_lock)
self.exiting = False
self._crash_handler = None # type: Optional[Callable]
self._transport_fail_handler = None # type: Optional[Callable]
self._error_display_handler = lambda msg: debug(msg)
self.settings = settings

def send_request(self, request: Request, handler: 'Callable[[Optional[Any]], None]',
error_handler: 'Optional[Callable[[Any], None]]' = None) -> None:
def send_request(
self,
request: Request,
handler: 'Callable[[Optional[Any]], None]',
error_handler: 'Optional[Callable[[Any], None]]' = None,
) -> 'None':
self.request_id += 1
if self.transport is not None:
debug(' --> ' + request.method)
Expand All @@ -92,6 +101,30 @@ def send_request(self, request: Request, handler: 'Callable[[Optional[Any]], Non
debug('unable to send', request.method)
if error_handler is not None:
error_handler(None)
return None

def execute_request(self, request: Request, timeout: float = DEFAULT_SYNC_REQUEST_TIMEOUT) -> 'Optional[Any]':
"""
Sends a request and waits for response up to timeout (default: 1 second), blocking the current thread.
"""
if self.transport is None:
debug('unable to send', request.method)
return None

debug(' ==> ' + request.method)
self.request_id += 1
request_id = self.request_id
self.send_payload(request.to_payload(request_id))
result = None
try:
with self._sync_request_cvar:
# We go to sleep. We wake up once another thread calls .notify() on this condition variable.
self._sync_request_cvar.wait_for(lambda: request_id in self._sync_request_results, timeout)
result = self._sync_request_results.pop(request_id)
except KeyError as e:
debug('timeout on', request.method)
return None
return result

def send_notification(self, notification: Notification) -> None:
if self.transport is not None:
Expand Down Expand Up @@ -159,6 +192,8 @@ def on_transport_closed(self) -> None:
self.handle_transport_failure()

def response_handler(self, response: 'Dict[str, Any]') -> None:
# This response handler *must not* run from the same thread that does a sync request
# because of the usage of the condition variable below.
request_id = int(response["id"])
if self.settings.log_payloads:
debug(' ' + str(response.get("result", None)))
Expand All @@ -167,7 +202,10 @@ def response_handler(self, response: 'Dict[str, Any]') -> None:
if handler:
handler(response["result"])
else:
debug("No handler found for id", request_id)
with self._sync_request_cvar:
self._sync_request_results[request_id] = response["result"]
# At most one thread is waiting on the result.
self._sync_request_cvar.notify()
elif "result" not in response and "error" in response:
error = response["error"]
if self.settings.log_payloads:
Expand Down
3 changes: 2 additions & 1 deletion plugin/core/sessions.py
Expand Up @@ -67,7 +67,8 @@ def get_initialize_params(project_path: str, config: ClientConfig) -> dict:
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": True
"didSave": True,
"willSaveWaitUntil": True
},
"hover": {
"contentFormat": ["markdown", "plaintext"]
Expand Down
89 changes: 79 additions & 10 deletions plugin/formatting.py
@@ -1,9 +1,14 @@
import sublime
import sublime_plugin
from .core.protocol import Request
from .core.configurations import is_supported_syntax
from .core.settings import client_configs
from .core.edit import parse_text_edit
from .core.registry import LspTextCommand
from .core.registry import LspTextCommand, session_for_view, client_from_session, sessions_for_view
from .core.url import filename_to_uri
from .core.sessions import Session
from .core.views import region_to_range
from .core.events import global_events

try:
from typing import Dict, Any, List, Optional
Expand All @@ -13,25 +18,90 @@


def options_for_view(view: sublime.View) -> 'Dict[str, Any]':
return {
"tabSize": view.settings().get("tab_size", 4),
"insertSpaces": True
}
return {"tabSize": view.settings().get("tab_size", 4), "insertSpaces": True}


def apply_response_to_view(response: 'Optional[List[dict]]', view: sublime.View) -> None:
edits = list(parse_text_edit(change) for change in response) if response else []
view.run_command('lsp_apply_document_edit', {'changes': edits})


def wants_will_save_wait_until(session: Session) -> bool:
sync_options = session.capabilities.get("textDocumentSync")
if isinstance(sync_options, dict):
if sync_options.get('willSaveWaitUntil'):
return True
return False


def run_will_save_wait_until(view: sublime.View, file_path: str, session: Session) -> None:
client = client_from_session(session)
if client:
# Make sure that the server sees the most recent document changes.
global_events.publish("view.on_purge_changes", view)
params = {
"textDocument": {
"uri": filename_to_uri(file_path)
},
"reason": 1 # TextDocumentSaveReason.Manual
}
request = Request.willSaveWaitUntil(params)
response = client.execute_request(request)
if response:
apply_response_to_view(response, view)
global_events.publish("view.on_purge_changes", view)


def run_format_on_save(view: sublime.View, file_path: str) -> None:
client = client_from_session(session_for_view(view, 'documentFormattingProvider'))
if client:
# Make sure that the server sees the most recent document changes.
global_events.publish("view.on_purge_changes", view)
params = {
"textDocument": {
"uri": filename_to_uri(file_path)
},
"options": options_for_view(view)
}
request = Request.formatting(params)
response = client.execute_request(request)
if response:
apply_response_to_view(response, view)
global_events.publish("view.on_purge_changes", view)


class FormatOnSaveListener(sublime_plugin.ViewEventListener):
def __init__(self, view: sublime.View) -> None:
super().__init__(view)

@classmethod
def is_applicable(cls, view_settings: dict) -> bool:
syntax = view_settings.get('syntax')
if syntax:
return is_supported_syntax(syntax, client_configs.all)
return False

def on_pre_save(self) -> None:
file_path = self.view.file_name()
if not file_path:
return

for session in sessions_for_view(self.view):
if wants_will_save_wait_until(session):
run_will_save_wait_until(self.view, file_path, session)

if self.view.settings().get("lsp_format_on_save"):
run_format_on_save(self.view, file_path)


class LspFormatDocumentCommand(LspTextCommand):
def __init__(self, view: sublime.View) -> None:
super().__init__(view)

def is_enabled(self, event: 'Optional[dict]' = None) -> bool:
return self.has_client_with_capability('documentFormattingProvider')

def run(self, edit: 'Any') -> None:
def run(self, edit: sublime.Edit) -> None:
client = self.client_with_capability('documentFormattingProvider')
file_path = self.view.file_name()
if client and file_path:
Expand All @@ -42,8 +112,7 @@ def run(self, edit: 'Any') -> None:
"options": options_for_view(self.view)
}
request = Request.formatting(params)
client.send_request(
request, lambda response: apply_response_to_view(response, self.view))
client.send_request(request, lambda response: apply_response_to_view(response, self.view))


class LspFormatDocumentRangeCommand(LspTextCommand):
Expand All @@ -70,5 +139,5 @@ def run(self, edit: sublime.Edit) -> None:
"range": region_to_range(self.view, region).to_lsp(),
"options": options_for_view(self.view)
}
client.send_request(Request.rangeFormatting(params),
lambda response: apply_response_to_view(response, self.view))
client.send_request(
Request.rangeFormatting(params), lambda response: apply_response_to_view(response, self.view))

0 comments on commit 0d2e843

Please sign in to comment.