diff --git a/LSP.sublime-settings b/LSP.sublime-settings index e10de6510..1200f2155 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -13,14 +13,16 @@ // // The command line required to run the server. // "command": ["pyls"], // - // // Use: "Show Scope Name" from Sublime's Developer menu - // "scopes": ["source.python"], - // - // // Run: view.settings().get("syntax") in console - // "syntaxes": ["Packages/Python/Python.sublime-syntax"], - // // // See: https://github.com/Microsoft/language-server-protocol/issues/213 - // "languageId": "python", + // "languages": { + // "python": { + // // Use: "Show Scope Name" from Sublime's Developer menu + // "scopes": ["source.python"], + // + // // Run: view.settings().get("syntax") in console + // "syntaxes": ["Packages/Python/Python.sublime-syntax"], + // } + // }, // // # Optional settings (key-value pairs): // @@ -46,44 +48,74 @@ "pyls": { "command": ["pyls"], - "scopes": ["source.python"], - "syntaxes": ["Packages/Python/Python.sublime-syntax"], - "languageId": "python" + "languages": { + "python": { + "scopes": ["source.python"], + "syntaxes": ["Packages/Python/Python.sublime-syntax"] + } + } }, "rls": { "command": ["rustup", "run", "nightly", "rls"], - "scopes": ["source.rust"], - "syntaxes": ["Packages/Rust/Rust.sublime-syntax", "Packages/Rust Enhanced/RustEnhanced.sublime-syntax"], - "languageId": "rust" + "languages": { + "rust": { + "scopes": ["source.rust"], + "syntaxes": ["Packages/Rust/Rust.sublime-syntax", "Packages/Rust Enhanced/RustEnhanced.sublime-syntax"] + } + } }, "clangd": { "command": ["clangd"], - "scopes": ["source.c", "source.c++", "source.objc", "source.objc++"], - "syntaxes": ["Packages/C++/C.sublime-syntax", "Packages/C++/C++.sublime-syntax", "Packages/Objective-C/Objective-C.sublime-syntax", "Packages/Objective-C/Objective-C++.sublime-syntax"], - "languageId": "objc++" + "languages": { + "c": { + "scopes": ["source.c"], + "syntaxes": ["Packages/C++/C.sublime-syntax"] + }, + "cpp": { + "scopes": ["source.c++"], + "syntaxes": ["Packages/C++/C++.sublime-syntax"] + }, + "objc": { + "scopes": ["source.objc"], + "syntaxes": ["Packages/Objective-C/Objective-C.sublime-syntax"] + }, + "objc++": { + "scopes": ["source.objc++"], + "syntaxes": ["Packages/Objective-C/Objective-C++.sublime-syntax"] + } + } }, "reason": { "command": ["ocaml-language-server", "--stdio"], - "scopes": ["source.reason"], - "syntaxes": ["Packages/sublime-reason/Reason.tmLanguage"], - "languageId": "reason" + "languages": { + "reason": { + "scopes": ["source.reason"], + "syntaxes": ["Packages/sublime-reason/Reason.tmLanguage"] + } + } }, "phpls": { "command": ["php", "~/vendor/felixfbecker/language-server/bin/php-language-server.php"], - "scopes": ["source.php", "embedding.php"], - "syntaxes": ["Packages/PHP/PHP.sublime-syntax"], - "languageId": "php" + "languages": { + "php": { + "scopes": ["source.php", "embedding.php"], + "syntaxes": ["Packages/PHP/PHP.sublime-syntax"] + } + } }, "eslint": { "command": ["node", "/Users/tomv/Projects/tomv564/vscode-eslint/eslint-server/src/server"], - "scopes": ["source.js"], - "syntaxes": ["Packages/Babel/JavaScript (Babel).sublime-syntax", "Packages/JavaScript/JavaScript.sublime-syntax"], - "languageId": "javascript", + "languages": { + "javascript": { + "scopes": ["source.js"], + "syntaxes": ["Packages/Babel/JavaScript (Babel).sublime-syntax", "Packages/JavaScript/JavaScript.sublime-syntax"] + } + }, "initializationOptions": { "nodePath": "/usr/local/bin/node" } @@ -91,42 +123,54 @@ "ocaml": { "command": ["ocaml-language-server", "--stdio"], - "scopes": ["source.ocaml"], - "syntaxes": ["Packages/OCaml/OCaml.sublime-syntax"], - "languageId": "ocaml" + "languages": { + "ocaml": { + "scopes": ["source.ocaml"], + "syntaxes": ["Packages/OCaml/OCaml.sublime-syntax"] + } + } }, "golsp": { "command": ["go-langserver"], - "scopes": ["source.go"], - "syntaxes": ["Packages/Go/Go.sublime-syntax"], - "languageId": "go" + "languages": { + "go": { + "scopes": ["source.go"], + "syntaxes": ["Packages/Go/Go.sublime-syntax"] + } + } }, "jdtls": { "command": ["java", "-jar", "PATH_TO_JDT_SERVER/plugins/org.eclipse.equinox.launcher_1.4.0.v20161219-1356.jar", "-configuration", "PATH_TO_CONFIG_DIR"], - "scopes": ["source.java"], - "syntaxes": ["Packages/Java/Java.sublime-syntax"], - "languageId": "java" + "languages": { + "java": { + "scopes": ["source.java"], + "syntaxes": ["Packages/Java/Java.sublime-syntax"] + } + } }, "polymer-ide": { "command": ["polymer-editor-service"], - "scopes": [ - "text.html.basic", - "text.html", - "source.html", - "source.js", - "source.css", - "source.json" - ], - "syntaxes": [ - "Packages/HTML/HTML.sublime-syntax", - "Packages/CSS/CSS.sublime-syntax", - "Packages/JavaScript/JavaScript.sublime-syntax", - "Packages/JavaScript/JSON.sublime-syntax" - ], - "languageId": "javascript", + "languages": { + "javascript": { + "scopes": [ + "text.html.basic", + "text.html", + "source.html", + "source.js", + "source.css", + "source.json" + ], + "syntaxes": [ + "Packages/HTML/HTML.sublime-syntax", + "Packages/CSS/CSS.sublime-syntax", + "Packages/JavaScript/JavaScript.sublime-syntax", + "Packages/JavaScript/JSON.sublime-syntax" + ] + } + }, "settings": { "polymer-ide": { "analyzeWholePackage": false, @@ -137,16 +181,17 @@ "rlang": { "command": ["R", "--quiet", "--slave", "-e", "languageserver::run()"], - "languageId": "r", - "scopes": - [ - "source.r" - ], - "syntaxes": - [ - "Packages/R/R.sublime-syntax", - "Packages/R-Box/syntax/R Extended.sublime-syntax" - ] + "languages": { + "r": { + "scopes": [ + "source.r" + ], + "syntaxes": [ + "Packages/R/R.sublime-syntax", + "Packages/R-Box/syntax/R Extended.sublime-syntax" + ] + } + } } }, @@ -164,7 +209,7 @@ // Show in-line diagnostics using phantoms for unchanged files. "show_diagnostics_phantoms": false, - // Show errors and warnings count in the status bar + // Show errors and warnings count in the status bar "show_diagnostics_count_in_view_status": false, // Show the diagnostics description of the code @@ -225,7 +270,7 @@ "log_debug": false, // Show notifications from language servers in the console. - "log_server": true, + "log_server": false, // Show language server stderr output in the console. "log_stderr": false, diff --git a/docs/index.md b/docs/index.md index 635d354c4..721ddde98 100644 --- a/docs/index.md +++ b/docs/index.md @@ -71,9 +71,12 @@ Client configuration: "flow": { "command": ["flow-language-server", "--stdio"], - "scopes": ["source.js"], - "syntaxes": ["Packages/Babel/JavaScript (Babel).sublime-syntax", "Packages/JavaScript/JavaScript.sublime-syntax"], - "languageId": "javascript" + "languages": { + "javascript": { + "scopes": ["source.js"], + "syntaxes": ["Packages/Babel/JavaScript (Babel).sublime-syntax", "Packages/JavaScript/JavaScript.sublime-syntax"], + } + } } ``` @@ -89,16 +92,19 @@ Client configuration: "/ABSOLUTE/PATH/TO/SERVER/.npm-global/bin/vls" ], "enabled": true, - "languageId": "vue", - "scopes": [ - "text.html.vue" - ], - "syntaxes": [ - // For ST3 builds < 3153 - "Packages/Vue Syntax Highlight/vue.tmLanguage" - // For ST3 builds >= 3153 - // "Packages/Vue Syntax Highlight/Vue Component.sublime-syntax" - ] + "languages": { + "vue": { + "scopes": [ + "text.html.vue" + ], + "syntaxes": [ + // For ST3 builds < 3153 + "Packages/Vue Syntax Highlight/vue.tmLanguage" + // For ST3 builds >= 3153 + // "Packages/Vue Syntax Highlight/Vue Component.sublime-syntax" + ] + } + } } ``` @@ -133,9 +139,12 @@ UPDATE: Some new options for PHP language servers are discussed in [this issue]( "clients": { "phpls": { "command": ["php", "/PATH-TO-HOME-DIR/.composer/vendor/felixfbecker/language-server/bin/php-language-server.php"], - "scopes": ["source.php"], - "syntaxes": ["Packages/PHP/PHP.sublime-syntax"], - "languageId": "php" + "languages": { + "php": { + "scopes": ["source.php"], + "syntaxes": ["Packages/PHP/PHP.sublime-syntax"] + } + } } } } @@ -248,9 +257,12 @@ Client configuration: "golsp": { "command": ["go-langserver"], - "scopes": ["source.go"], - "syntaxes": ["Packages/Go/Go.sublime-syntax"], - "languageId": "go" + "languages": { + "go": { + "scopes": ["source.go"], + "syntaxes": ["Packages/Go/Go.sublime-syntax"] + } + } }, ``` @@ -266,9 +278,12 @@ Then add to your LSP settings (replace PATH_TO_NODE_MODULES): "vscode-css": { "command": ["node", "PATH_TO_NODE_MODULES/vscode-css-languageserver-bin/cssServerMain.js", "--stdio"], - "scopes": ["source.css"], - "syntaxes": ["Packages/CSS/CSS.sublime-syntax"], - "languageId": "css" + "languages": { + "css": { + "scopes": ["source.css"], + "syntaxes": ["Packages/CSS/CSS.sublime-syntax"] + } + } }, ``` @@ -301,18 +316,21 @@ LSP ships with default client configuration for a few language servers. Here is ```json "jsts": { "command": ["javascript-typescript-stdio"], - "scopes": ["source.ts", "source.tsx"], - "syntaxes": ["Packages/TypeScript-TmLanguage/TypeScript.tmLanguage", "Packages/TypeScript-TmLanguage/TypeScriptReact.tmLanguage"], - "languageId": "typescript" + "languages": { + "typescript": { + "scopes": ["source.ts", "source.tsx"], + "syntaxes": ["Packages/TypeScript-TmLanguage/TypeScript.tmLanguage", "Packages/TypeScript-TmLanguage/TypeScriptReact.tmLanguage"] + } + } } ``` These can be customized as follows by adding an override in the User LSP.sublime-settings * `command` - specify a full paths, add arguments -* `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` - used both by the language servers and to select a syntax highlighter for sublime popups. +* `languages` - used both by the language servers and to select a syntax highlighter for sublime popups. +* `languages..scopes` - add language flavours, eg. `source.js`, `source.jsx`. +* `languages..syntaxes` - syntaxes that enable LSP features on a document, eg. `Packages/Babel/JavaScript (Babel).tmLanguage` * `enabled` - disable a language server globally, or per-project * `settings` - per-project settings (equivalent to VS Code's Workspace Settings) * `env` - dict of environment variables to be injected into the language server's process (eg. PYTHONPATH) diff --git a/plugin/completion.py b/plugin/completion.py index 9f94b0bef..cda3675be 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -16,9 +16,38 @@ from .core.documents import get_document_position, purge_did_change -NO_COMPLETION_SCOPES = 'comment, string' +NO_COMPLETION_SCOPES = 'comment' completion_item_kind_names = {v: k for k, v in CompletionItemKind.__dict__.items()} +completion_item_kind_icons = { + None: "💭", + 1: "📝", # Text + 2: "⚙", # Method + 3: "⚙", # Function + 4: "⚙", # Constructor + 5: "🏷", # Field + 6: "🏷", # Variable + 7: "🗳", # Class + 8: "🗳", # Interface + 9: "📦", # Module + 10: "🔧", # Property + 11: "◼️", # Unit + 12: "🔹", # Value + 13: "🗂", # Enum + 14: "🔅", # Keyword + 15: "💊", # Snippet + 16: "🎨", # Color + 17: "📄", # File + 18: "🔸", # Reference + 19: "📁", # Folder + 20: "🔹", # EnumMember + 21: "⚓", # Constant + 22: "🗳", # Struct + 23: "🗓", # Event + 24: "⚙", # Operator + 25: "🏷", # TypeParameter +} + class CompletionState(object): IDLE = 0 @@ -130,9 +159,9 @@ def __init__(self, view): self.resolve_details = [] # type: List[Tuple[str, str]] self.state = CompletionState.IDLE self.completions = [] # type: List[Any] - self.next_request = None # type: Optional[Tuple[str, List[int]]] + self.next_request = None # type: Optional[int] self.last_prefix = "" - self.last_location = 0 + self.last_pos = 0 @classmethod def is_applicable(cls, settings): @@ -159,55 +188,74 @@ def is_after_trigger_character(self, location): prev_char = self.view.substr(location - 1) return prev_char in self.trigger_chars - def is_same_completion(self, prefix, locations): - # completion requests from the same location with the same prefix are cached. - current_start = locations[0] - len(prefix) - last_start = self.last_location - len(self.last_prefix) - return prefix.startswith(self.last_prefix) and current_start == last_start - - def on_modified(self): - # hide completion when backspacing past last completion. - if self.view.sel()[0].begin() < self.last_location: - self.last_location = 0 - self.view.run_command("hide_auto_complete") - # cancel current completion if the previous input is an space - prev_char = self.view.substr(self.view.sel()[0].begin() - 1) - if self.state == CompletionState.REQUESTING and prev_char.isspace(): - self.state = CompletionState.CANCELLING - - def on_query_completions(self, prefix, locations): - if self.view.match_selector(locations[0], NO_COMPLETION_SCOPES): - return ( - [], - sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS - ) - + def on_modified_async(self): if not self.initialized: self.initialize() - if self.enabled: - reuse_completion = self.is_same_completion(prefix, locations) - if self.state == CompletionState.IDLE: - if not reuse_completion: - self.last_prefix = prefix - self.last_location = locations[0] - self.do_request(prefix, locations) - self.completions = [] + if not self.enabled or not self.trigger_chars: + return + + view_sel = self.view.sel() + if not view_sel: + return + + pos = view_sel[0].begin() + if self.view.match_selector(pos, NO_COMPLETION_SCOPES): + return - elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING): - self.next_request = (prefix, locations) + prev_char = self.view.substr(pos - 1) + if prev_char in self.trigger_chars or prev_char == ' ': + # hide completion when backspacing past last completion. + if self.last_pos and pos < self.last_pos: + self.last_pos = 0 + self.view.run_command("hide_auto_complete") + # cancel current completion if the previous input is an space + if self.state == CompletionState.REQUESTING and prev_char.isspace(): self.state = CompletionState.CANCELLING - elif self.state == CompletionState.APPLYING: - self.state = CompletionState.IDLE + command_history = getattr(self.view, 'command_history', None) + if command_history: + redo_command = command_history(1) + previous_command = self.view.command_history(0) + before_previous_command = self.view.command_history(-1) + else: + redo_command = previous_command = before_previous_command = None + + # print('on_modified', "%r\n\tcommand_history: %r\n\tredo_command: %r\n\tprevious_command: %r\n\tbefore_previous_command: %r" % (prev_char, bool(command_history), redo_command, previous_command, before_previous_command)) + if not command_history or redo_command[1] is None and ( + previous_command[0] in ('paste', 'insert_completion') or + previous_command[0] == 'insert' and previous_command[1]['characters'][-1] not in ('\n', '\t') or + previous_command[0] == 'insert_snippet' and previous_command[1]['contents'] in ( + '(${0:$SELECTION})', '[${0:$SELECTION}]', '{${0:$SELECTION}}', '`${0:$SELECTION}`', '"${0:$SELECTION}"', "'${0:$SELECTION}'", + '($0)', '[$0]', '{$0}', '`$0`', '"$0"', "'$0'", + ) or + before_previous_command[0] in ('paste', 'insert') and ( + previous_command[0] == 'commit_completion' or + previous_command[0] == 'insert_completion' or + previous_command[0] == 'insert_best_completion' + ) + ): + if self.state == CompletionState.APPLYING: + self.state = CompletionState.IDLE + + if self.state == CompletionState.IDLE: + self.do_request(pos) + self.completions = [] + + elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING): + self.next_request = pos + self.state = CompletionState.CANCELLING + def on_query_completions(self, prefix, locations): + if self.completions: return ( self.completions, 0 if not settings.only_show_lsp_completions else sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS ) - def do_request(self, prefix: str, locations: 'List[int]'): + def do_request(self, pos: int): + self.last_pos = pos self.next_request = None view = self.view @@ -216,9 +264,9 @@ def do_request(self, prefix: str, locations: 'List[int]'): if not client: return - if settings.complete_all_chars or self.is_after_trigger_character(locations[0]): + if settings.complete_all_chars or self.is_after_trigger_character(pos): purge_did_change(view.buffer_id()) - document_position = get_document_position(view, locations[0]) + document_position = get_document_position(view, pos) if document_position: client.send_request( Request.complete(document_position), @@ -229,18 +277,18 @@ def do_request(self, prefix: str, locations: 'List[int]'): def format_completion(self, item: dict) -> 'Tuple[str, str]': # Sublime handles snippets automatically, so we don't have to care about insertTextFormat. label = item["label"] + kind = item.get("kind") + icon = completion_item_kind_icons.get(kind) or completion_item_kind_icons[None] # choose hint based on availability and user preference hint = None if settings.completion_hint_type == "auto": hint = item.get("detail") if not hint: - kind = item.get("kind") if kind: hint = completion_item_kind_names[kind] elif settings.completion_hint_type == "detail": hint = item.get("detail") elif settings.completion_hint_type == "kind": - kind = item.get("kind") if kind: hint = completion_item_kind_names.get(kind) # label is an alternative for insertText if neither textEdit nor insertText is provided @@ -248,7 +296,7 @@ def format_completion(self, item: dict) -> 'Tuple[str, str]': if len(insert_text) > 0 and insert_text[0] == '$': # sublime needs leading '$' escaped. insert_text = '\\$' + insert_text[1:] # only return label with a hint if available - return "\t ".join((label, hint)) if hint else label, insert_text + return "\t ".join((icon + " " + label, hint)) if hint else icon + " " + label, insert_text def text_edit_text(self, item) -> 'Optional[str]': # try to handle textEdit if present @@ -257,7 +305,7 @@ def text_edit_text(self, item) -> 'Optional[str]': edit_range, edit_text = text_edit.get("range"), text_edit.get("newText") if edit_range and edit_text: edit_range = Range.from_lsp(edit_range) - last_start = self.last_location - len(self.last_prefix) + last_start = self.last_pos - len(self.last_prefix) last_row, last_col = self.view.rowcol(last_start) if last_row == edit_range.start.row == edit_range.end.row and edit_range.start.col <= last_col: # sublime does not support explicit replacement with completion @@ -292,8 +340,7 @@ def handle_response(self, response: 'Optional[Dict]'): self.run_auto_complete() elif self.state == CompletionState.CANCELLING: if self.next_request: - prefix, locations = self.next_request - self.do_request(prefix, locations) + self.do_request(self.next_request) else: debug('Got unexpected response while in state {}'.format(self.state)) diff --git a/plugin/core/configurations.py b/plugin/core/configurations.py index fae96cd4c..997668c7f 100644 --- a/plugin/core/configurations.py +++ b/plugin/core/configurations.py @@ -17,22 +17,27 @@ window_client_configs = dict() # type: Dict[int, List[ClientConfig]] -def get_scope_client_config(view: 'sublime.View', configs: 'List[ClientConfig]') -> 'Optional[ClientConfig]': +def _get_scope_client_config(view: 'sublime.View', configs: 'List[ClientConfig]') -> 'Tuple[Optional[ClientConfig], int]': # When there are multiple server configurations, all of which are for # similar scopes (e.g. 'source.json', 'source.json.sublime.settings') the # configuration with the most specific scope (highest ranked selector) # in the current position is preferred. - scope_score = 0 scope_client_config = None - for config in configs: - for scope in config.scopes: - sel = view.sel() - if len(sel) > 0: - score = view.score_selector(sel[0].begin(), scope) + scope_score = 0 + sel = view.sel() + if len(sel) > 0: + pos = sel[0].begin() + for config in configs: + for scope in config.scopes: + score = view.score_selector(pos, scope) if score > scope_score: scope_score = score scope_client_config = config - return scope_client_config + return scope_client_config, scope_score + + +def get_scope_client_config(view: 'sublime.View', configs: 'List[ClientConfig]') -> 'Optional[ClientConfig]': + return _get_scope_client_config(view, configs)[0] def register_client_config(config: ClientConfig) -> None: @@ -59,12 +64,15 @@ def get_window_client_config(view: sublime.View) -> 'Optional[ClientConfig]': def config_for_scope(view: sublime.View) -> 'Optional[ClientConfig]': # check window_client_config first - window_client_config = get_window_client_config(view) - if not window_client_config: - global_client_config = get_global_client_config(view) - + window = view.window() + if window: + configs_for_window = window_client_configs.get(window.id(), []) + window_client_config, window_score = _get_scope_client_config(view, configs_for_window) + else: + window_client_config, window_score = None, 0 + global_client_config, global_score = _get_scope_client_config(view, client_configs.all) + if not window_client_config or global_score > window_score: if global_client_config: - window = view.window() if window: window_client_config = apply_window_settings(global_client_config, view) add_window_client_config(window, window_client_config) @@ -99,9 +107,7 @@ def apply_window_settings(client_config: 'ClientConfig', view: 'sublime.View') - client_config.name, overrides.get("command", client_config.binary_args), overrides.get("tcp_port", client_config.tcp_port), - overrides.get("scopes", client_config.scopes), - overrides.get("syntaxes", client_config.syntaxes), - overrides.get("languageId", client_config.languageId), + overrides.get("languages", client_config.languages), overrides.get("enabled", client_config.enabled), overrides.get("initializationOptions", client_config.init_options), overrides.get("settings", client_config.settings), diff --git a/plugin/core/documents.py b/plugin/core/documents.py index 29bbd74da..0bd5caf2c 100644 --- a/plugin/core/documents.py +++ b/plugin/core/documents.py @@ -59,6 +59,7 @@ class DocumentState: def __init__(self, path: str) -> 'None': self.path = path self.version = 0 + self.languageId = None def inc_version(self): self.version += 1 @@ -132,12 +133,13 @@ def notify_did_open(view: sublime.View): if window and view_file: if not has_document_state(window, view_file): ds = get_document_state(window, view_file) + ds.languageId = config.get_language_id(view) if settings.show_view_status: view.set_status("lsp_clients", config.name) params = { "textDocument": { "uri": filename_to_uri(view_file), - "languageId": config.languageId, + "languageId": ds.languageId, "text": view.substr(sublime.Region(0, view.size())), "version": ds.version } @@ -176,22 +178,37 @@ def notify_did_change(view: sublime.View): if window and file_name: if view.buffer_id() in pending_buffer_changes: del pending_buffer_changes[view.buffer_id()] - # config = config_for_scope(view) + config = config_for_scope(view) client = client_for_view(view) - if client: - document_state = get_document_state(window, file_name) + if client and config: uri = filename_to_uri(file_name) - params = { - "textDocument": { - "uri": uri, - # "languageId": config.languageId, clangd does not like this field, but no server uses it? - "version": document_state.inc_version(), - }, - "contentChanges": [{ - "text": view.substr(sublime.Region(0, view.size())) - }] - } - client.send_notification(Notification.didChange(params)) + languageId = config.get_language_id(view) + ds = get_document_state(window, file_name) + if ds.languageId == languageId: + params = { + "textDocument": { + "uri": uri, + "version": ds.inc_version(), + }, + "contentChanges": [{ + "text": view.substr(sublime.Region(0, view.size())) + }] + } + client.send_notification(Notification.didChange(params)) + else: + # The languageId has changed, reopen file + ds.languageId = languageId + params = {"textDocument": {"uri": uri}} + client.send_notification(Notification.didClose(params)) + params = { + "textDocument": { + "uri": uri, + "languageId": ds.languageId, + "text": view.substr(sublime.Region(0, view.size())), + "version": ds.inc_version(), + } + } + client.send_notification(Notification.didOpen(params)) document_sync_initialized = False diff --git a/plugin/core/main.py b/plugin/core/main.py index 802cd1b7e..2b0c1ac2f 100644 --- a/plugin/core/main.py +++ b/plugin/core/main.py @@ -166,7 +166,7 @@ def handle_session_started(session, window, project_path, config): client.send_notification(Notification.initialized()) if config.settings: configParams = { - 'settings': config.settings + 'settings': config.get_settings(window) } client.send_notification(Notification.didChangeConfiguration(configParams)) diff --git a/plugin/core/rpc.py b/plugin/core/rpc.py index 6413c6fc7..431725836 100644 --- a/plugin/core/rpc.py +++ b/plugin/core/rpc.py @@ -18,6 +18,14 @@ TCP_CONNECT_TIMEOUT = 5 +def ordereddict_to_dict(value: 'Dict[str, Any]'): + value = dict(value) + for k, v in value.items(): + if isinstance(v, dict): + value[k] = ordereddict_to_dict(v) + return value + + def format_request(payload: 'Dict[str, Any]'): """Converts the request into json and adds the Content-Length header""" content = json.dumps(payload, sort_keys=False) @@ -72,10 +80,10 @@ def __init__(self, transport, settings): self.transport = transport self.transport.start(self.receive_payload, self.on_transport_closed) self.request_id = 0 - self._response_handlers = {} # type: Dict[int, Callable] - self._error_handlers = {} # type: Dict[int, Callable] - self._request_handlers = {} # type: Dict[str, Callable] - self._notification_handlers = {} # type: Dict[str, Callable] + self._response_handlers = {} # type: Dict[int, List[Callable]] + self._error_handlers = {} # type: Dict[int, List[Callable]] + self._request_handlers = {} # type: Dict[str, List[Callable]] + self._notification_handlers = {} # type: Dict[str, List[Callable]] self.exiting = False self._crash_handler = None # type: Optional[Callable] self._transport_fail_handler = None # type: Optional[Callable] @@ -84,15 +92,19 @@ def __init__(self, transport, settings): def send_request(self, request: Request, handler: 'Callable', error_handler: 'Optional[Callable]' = None): self.request_id += 1 - debug(' --> ' + request.method) + debug(' >>> ' + request.method) + if self.settings.log_payloads and request.params: + debug(' --> ' + str(ordereddict_to_dict(request.params))) if handler is not None: - self._response_handlers[self.request_id] = handler + self._response_handlers.setdefault(self.request_id, []).append(handler) if error_handler is not None: - self._error_handlers[self.request_id] = error_handler + self._error_handlers.setdefault(self.request_id, []).append(error_handler) self.send_payload(request.to_payload(self.request_id)) def send_notification(self, notification: Notification): - debug(' --> ' + notification.method) + debug(' >>> ' + notification.method) + if self.settings.log_payloads and notification.params: + debug(' --> ' + str(ordereddict_to_dict(notification.params))) self.send_payload(notification.to_payload()) def exit(self): @@ -158,39 +170,42 @@ def response_handler(self, response): if 'result' in response and 'error' not in response: result = response['result'] if self.settings.log_payloads: - debug(' ' + str(result)) + debug(' <-- ' + str(result)) if handler_id in self._response_handlers: - self._response_handlers[handler_id](result) + for handler in self._response_handlers[handler_id]: + handler(result) else: debug("No handler found for id " + str(response.get("id"))) elif 'error' in response and 'result' not in response: error = response['error'] if self.settings.log_payloads: - debug(' ' + str(error)) + debug(' <-- ' + str(error)) if handler_id in self._error_handlers: - self._error_handlers[handler_id](error) + for handler in self._error_handlers[handler_id]: + handler(error) else: self._error_display_handler(error.get("message")) else: - debug('invalid response payload', response) + debug(' <-- [invalid response payload]', response) def on_request(self, request_method: str, handler: 'Callable'): - self._request_handlers[request_method] = handler + self._request_handlers.setdefault(request_method, []).append(handler) def on_notification(self, notification_method: str, handler: 'Callable'): - self._notification_handlers[notification_method] = handler + self._notification_handlers.setdefault(notification_method, []).append(handler) def request_handler(self, request): params = request.get("params") method = request.get("method") - debug('<-- ' + method) + debug(' <<< ' + method) if self.settings.log_payloads and params: - debug(' ' + str(params)) + debug(' <-- ' + str(params)) if method in self._request_handlers: - try: - self._request_handlers[method](params) - except Exception as err: - exception_log("Error handling request " + method, err) + for handler in self._request_handlers[method]: + try: + handler(params) + except Exception as err: + exception_log("Error handling request " + method, err) else: debug("Unhandled request", method) @@ -198,13 +213,14 @@ def notification_handler(self, notification): method = notification.get("method") params = notification.get("params") if method != "window/logMessage": - debug('<-- ' + method) + debug(' <<< ' + method) if self.settings.log_payloads and params: - debug(' ' + str(params)) + debug(' <-- ' + str(params)) if method in self._notification_handlers: - try: - self._notification_handlers[method](params) - except Exception as err: - exception_log("Error handling notification " + method, err) + for handler in self._notification_handlers[method]: + try: + handler(params) + except Exception as err: + exception_log("Error handling notification " + method, err) else: debug("Unhandled notification:", method) diff --git a/plugin/core/settings.py b/plugin/core/settings.py index a92537e1d..09f9553e2 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -89,7 +89,6 @@ def update(self, settings_obj: sublime.Settings): self.all.extend(self._external_configs) def add_external_config(self, config: ClientConfig): - print('adding ', config.name) if config.name in self._global_settings: config.apply_settings(self._global_settings[config.name]) self._external_configs.append(config) @@ -135,9 +134,7 @@ def read_client_config(name, client_config): name, client_config.get("command", []), client_config.get("tcp_port", None), - client_config.get("scopes", []), - client_config.get("syntaxes", []), - client_config.get("languageId", ""), + client_config.get("languages", dict()), client_config.get("enabled", True), client_config.get("initializationOptions", dict()), client_config.get("settings", dict()), @@ -157,7 +154,7 @@ def read_client_configs(client_settings, default_client_settings=None) -> 'List[ client_with_defaults.update(client_config) config = read_client_config(client_name, client_with_defaults) - if config and config.scopes: # don't return configs only containing "enabled" here. + if config and config.languages: # don't return configs only containing "enabled" here. parsed_configs.append(config) return parsed_configs else: diff --git a/plugin/core/spinner.py b/plugin/core/spinner.py new file mode 100644 index 000000000..a44668af9 --- /dev/null +++ b/plugin/core/spinner.py @@ -0,0 +1,179 @@ +import sublime + + +class Spinner(object): + # From https://github.com/ManrajGrover/py-spinners/blob/master/spinners/spinners.py + spinners = { + "default": { + "interval": 100, + "frames": [ + "◐", + "◓", + "◑", + "◒", + ], + }, + "dots": { + "interval": 80, + "frames": [ + "⠋", + "⠙", + "⠹", + "⠸", + "⠼", + "⠴", + "⠦", + "⠧", + "⠇", + "⠏", + ], + }, + "line": { + "interval": 130, + "frames": [ + "-", + "\\", + "|", + "/", + ], + }, + "bouncingBall": { + "interval": 80, + "frames": [ + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "( ●)", + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "(● )", + ], + }, + "point": { + "interval": 125, + "frames": [ + "∙∙∙", + "●∙∙", + "∙●∙", + "∙∙●", + "∙∙∙", + ], + }, + "fire": { + "interval": 60, + "frames": [ + "🔥", + "🔥", + "🔥", + "🔥", + "🔥", + "🔥", + "➖", + ], + }, + "lightBulb": { + "interval": 200, + "frames": [ + "💡", + "💡", + "💡", + "➖", + ], + }, + "hourGlass": { + "interval": 300, + "frames": [ + "⏳", + "⌛", + ], + }, + "monkey": { + "interval": 300, + "frames": [ + "🙈", + "🙈", + "🙉", + "🙊", + ], + }, + "earth": { + "interval": 180, + "frames": [ + "🌍", + "🌎", + "🌏", + ], + }, + "clock": { + "interval": 100, + "frames": [ + "🕛", + "🕐", + "🕑", + "🕒", + "🕓", + "🕔", + "🕕", + "🕖", + "🕗", + "🕘", + "🕙", + "🕚", + ] + }, + "moon": { + "interval": 80, + "frames": [ + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖", + "🌗", + "🌘", + ], + }, + } + + def __init__(self): + self.key = 0 + self.frame = 0 + self.enabled = False + + def animate(self): + if self.enabled: + spinner = self.spinners[self.enabled] + interval = spinner['interval'] + frames = spinner['frames'] + sublime.set_timeout_async(self.animate, interval) + self.frame = (self.frame + 1) % len(frames) + sublime.status_message(self.prefix + " " + frames[self.frame] + " " + self.suffix) + + def deanimate(self, key): + if self.key == key: + self.stop() + + def start(self, prefix="", suffix="", timeout=2000, spinner='default'): + key = self.key = self.key + 1 + self.prefix = prefix + self.suffix = suffix + animated, self.enabled = self.enabled, spinner + if not animated: + self.animate() + if timeout > 0: + sublime.set_timeout_async(lambda: self.deanimate(key), timeout) + + def stop(self, prefix="", suffix=""): + self.key = 0 + self.prefix = prefix + self.suffix = suffix + animated, self.enabled = self.enabled, False + if animated: + sublime.status_message(self.prefix + " " + self.suffix) + + +spinner = Spinner() diff --git a/plugin/core/types.py b/plugin/core/types.py index 4397a16de..54eb101af 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -44,30 +44,36 @@ def __init__(self, project_path, state=ClientStates.STARTING, client=None, capab class ClientConfig(object): - def __init__(self, name, binary_args, tcp_port, scopes, syntaxes, languageId, + def __init__(self, name, binary_args, tcp_port, languages, enabled=True, init_options=dict(), settings=dict(), env=dict()): self.name = name self.binary_args = binary_args self.tcp_port = tcp_port - self.scopes = scopes - self.syntaxes = syntaxes - self.languageId = languageId + self.languages = languages self.enabled = enabled self.init_options = init_options self.settings = settings self.env = env + @property + def syntaxes(self): + for language_id, language in self.languages.items(): + for syntax in language.get('syntaxes', []): + yield syntax + + @property + def scopes(self): + for language_id, language in self.languages.items(): + for scope in language.get('scopes', []): + yield scope + def apply_settings(self, settings: dict) -> None: if "command" in settings: self.binary_args = settings.get("command", []) if "tcp_port" in settings: self.tcp_port = settings.get("tcp_port", None) - if "scopes" in settings: - self.scopes = settings.get("scopes", []) - if "syntaxes" in settings: - self.syntaxes = settings.get("syntaxes", []) - if "languageId" in settings: - self.languageId = settings.get("languageId", "") + if "languages" in settings: + self.languages = settings.get("languages", "") if "enabled" in settings: self.enabled = settings.get("enabled", True) if "initializationOptions" in settings: @@ -76,3 +82,20 @@ def apply_settings(self, settings: dict) -> None: self.settings = settings.get("settings", dict()) if "env" in settings: self.env = settings.get("env", dict()) + + def get_settings(self, window): + return self.settings + + def get_language_id(self, view): + scope_language_id = None + scope_score = 0 + sel = view.sel() + if len(sel) > 0: + pos = sel[0].begin() + for language_id, language in self.languages.items(): + for scope in language.get('scopes', []): + score = view.score_selector(pos, scope) + if score > scope_score: + scope_language_id = language_id + scope_score = score + return scope_language_id diff --git a/plugin/definition.py b/plugin/definition.py index e9ccab381..7a9097724 100644 --- a/plugin/definition.py +++ b/plugin/definition.py @@ -31,12 +31,14 @@ def handle_response(self, response, position): window = sublime.active_window() if response: location = response if isinstance(response, dict) else response[0] - file_path = uri_to_filename(location.get("uri")) - start = Point.from_lsp(location['range']['start']) - file_location = "{}:{}:{}".format(file_path, start.row + 1, start.col + 1) - debug("opening location", location) - window.open_file(file_location, sublime.ENCODED_POSITION) - # TODO: can add region here. + uri = location.get("uri") + if uri: + file_path = uri_to_filename(uri) + start = Point.from_lsp(location['range']['start']) + file_location = "{}:{}:{}".format(file_path, start.row + 1, start.col + 1) + debug("opening location", location) + window.open_file(file_location, sublime.ENCODED_POSITION) + # TODO: can add region here. else: window.run_command("goto_definition") diff --git a/plugin/highlights.py b/plugin/highlights.py index 1d6159113..7e4d81ef1 100644 --- a/plugin/highlights.py +++ b/plugin/highlights.py @@ -15,7 +15,7 @@ pass SUBLIME_WORD_MASK = 515 -NO_HIGHLIGHT_SCOPES = 'comment, string' +NO_HIGHLIGHT_SCOPES = 'comment' _kind2name = { DocumentHighlightKind.Unknown: "unknown", diff --git a/plugin/hover.py b/plugin/hover.py index ee3903004..275187487 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -11,7 +11,7 @@ from .core.popups import popup_css, popup_class SUBLIME_WORD_MASK = 515 -NO_HOVER_SCOPES = 'comment, string' +NO_HOVER_SCOPES = 'comment' class HoverHandler(sublime_plugin.ViewEventListener): @@ -79,7 +79,7 @@ def symbol_actions_content(self): def diagnostics_content(self, diagnostics): formatted_errors = list( - "
{}
".format(diagnostic.message) + "
{}
".format("[{}] {}".format(diagnostic.source, diagnostic.message) if diagnostic.source else "{}".format(diagnostic.message)) for diagnostic in diagnostics if diagnostic.severity == DiagnosticSeverity.Error) formatted = [] @@ -91,7 +91,7 @@ def diagnostics_content(self, diagnostics): formatted.append("") formatted_warnings = list( - "
{}
".format(diagnostic.message) + "
{}
".format("[{}] {}".format(diagnostic.source, diagnostic.message) if diagnostic.source else "{}".format(diagnostic.message)) for diagnostic in diagnostics if diagnostic.severity == DiagnosticSeverity.Warning) diff --git a/plugin/signature_help.py b/plugin/signature_help.py index 52139117f..5210a0e2e 100644 --- a/plugin/signature_help.py +++ b/plugin/signature_help.py @@ -20,6 +20,8 @@ from .core.popups import popup_css, popup_class from .core.settings import settings +NO_SIGNATURE_HELP_SCOPES = 'comment' + class SignatureHelpListener(sublime_plugin.ViewEventListener): @@ -49,26 +51,35 @@ def initialize(self): config = config_for_scope(self.view) if config: - self._language_id = config.languageId + self._language_id = config.get_language_id(self.view) self._initialized = True def on_modified_async(self): - pos = self.view.sel()[0].begin() # TODO: this will fire too often, narrow down using scopes or regex if not self._initialized: self.initialize() - if self._signature_help_triggers: - last_char = self.view.substr(pos - 1) - if last_char in self._signature_help_triggers: - self.request_signature_help(pos) - elif self._visible: - if last_char.isspace(): - # Peek behind to find the last non-whitespace character. - last_char = self.view.substr(self.view.find_by_class(pos, False, ~0) - 1) - if last_char not in self._signature_help_triggers: - self.view.hide_popup() + if not self._signature_help_triggers: + return + + view_sel = self.view.sel() + if not view_sel: + return + + pos = view_sel[0].begin() + if self.view.match_selector(pos, NO_SIGNATURE_HELP_SCOPES): + return + + prev_char = self.view.substr(pos - 1) + if prev_char in self._signature_help_triggers: + self.request_signature_help(pos) + elif self._visible: + if prev_char.isspace(): + # Peek behind to find the last non-whitespace character. + prev_char = self.view.substr(self.view.find_by_class(pos, False, ~0) - 1) + if prev_char not in self._signature_help_triggers: + self.view.hide_popup() def request_signature_help(self, point): client = client_for_view(self.view)