diff --git a/Cargo.lock b/Cargo.lock index 204a61ed..4c882a6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,11 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.1.0" @@ -375,7 +380,7 @@ dependencies = [ "jsonrpc-core 12.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", - "lsp-types 0.60.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lsp-types 0.70.0 (registry+https://github.com/rust-lang/crates.io-index)", "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "pathdiff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -448,9 +453,10 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.60.0" +version = "0.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1014,6 +1020,7 @@ dependencies = [ "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)" = "b5164d292487f037ece34ec0de2fcede2faa162f085dd96d2385ab81b12765ba" "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" "checksum cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "b548a4ee81fccb95919d4e22cfea83c7693ebfd78f0495493178db20b3139da7" "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" @@ -1057,7 +1064,7 @@ dependencies = [ "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum log-mdc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" "checksum log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "100052474df98158c0738a7d3f4249c99978490178b5f9f68cd835ac57adbd1b" -"checksum lsp-types 0.60.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fe3edefcd66dde1f7f1df706f46520a3c93adc5ca4bc5747da6621195e894efd" +"checksum lsp-types 0.70.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef197b24cb3f12fc3984667a505691fec9d683204ddff56f12b2d1940e09a988" "checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" diff --git a/Cargo.toml b/Cargo.toml index 927b4e41..56b1d304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ serde_derive = "1" serde_json = "1" crossbeam = "0.7.3" jsonrpc-core = "12" -lsp-types = "0.60" +lsp-types = { version = "0.70.0", features = ["proposed"] } url = "2" pathdiff = "0" diff = "0" diff --git a/autoload/LanguageClient.vim b/autoload/LanguageClient.vim index 6c1be135..9af785eb 100644 --- a/autoload/LanguageClient.vim +++ b/autoload/LanguageClient.vim @@ -251,6 +251,16 @@ function! s:MatchDelete(ids) abort endfor endfunction +function! s:ApplySemanticHighlights(bufnr, ns_id, clears, highlights) abort + for clear in a:clears + call nvim_buf_clear_namespace(a:bufnr, a:ns_id, clear.line_start, clear.line_end) + endfor + + for hl in a:highlights + call nvim_buf_add_highlight(a:bufnr, a:ns_id, hl.group, hl.line, hl.character_start, hl.character_end) + endfor +endfunction + " Batch version of nvim_buf_add_highlight function! s:AddHighlights(source, highlights) abort for hl in a:highlights @@ -1376,6 +1386,70 @@ function! LanguageClient_contextMenu() abort return LanguageClient_handleContextMenuItem(l:options[l:selection - 1]) endfunction +function! LanguageClient_showSemanticScopes(...) abort + let l:params = get(a:000, 0, {}) + let l:Callback = get(a:000, 1, function('s:print_semantic_scopes')) + + return LanguageClient#Call('languageClient/semanticScopes', l:params, l:Callback) +endfunction + +function! s:print_semantic_scopes(response) abort + let l:scope_mappings = a:response.result + + let l:msg = '' + for mapping in l:scope_mappings + let l:msg .= "Highlight Group:\n" + let l:msg .= ' ' . l:mapping.hl_group . "\n" + + let l:msg .= "Semantic Scope:\n" + let l:spaces = ' ' + for l:scope_name in l:mapping.scope + let l:msg .= l:spaces . l:scope_name . "\n" + let l:spaces .= ' ' + endfor + let l:msg .= "\n" + endfor + + echo l:msg +endfunction + +function! LanguageClient#showSemanticHighlightSymbols(...) abort + let l:params = get(a:000, 0, {}) + let l:Callback = get(a:000, 1, v:null) + + return LanguageClient#Call('languageClient/showSemanticHighlightSymbols', l:params, l:Callback) +endfunction + +function! LanguageClient_showCursorSemanticHighlightSymbols(...) abort + let l:params = get(a:000, 0, {}) + let l:Callback = get(a:000, 1, function('s:print_cursor_semantic_symbol')) + + return LanguageClient#showSemanticHighlightSymbols(l:params, l:Callback) +endfunction + +function! s:print_cursor_semantic_symbol(response) abort + let l:symbols = a:response.result + let l:lines = [] + + for symbol in l:symbols + if l:symbol.line + 1 == line('.') && + \ symbol.character_start < col('.') && + \ col('.') <= symbol.character_end + let l:spaces = '' + for scope_name in l:symbol.scope + call add(l:lines, l:spaces . l:scope_name) + let l:spaces .= ' ' + endfor + endif + endfor + + if len(l:lines) > 0 + call s:OpenHoverPreview('SemanticScopes', l:lines, 'text') + else + call s:Echowarn('No Symbol Under Cursor or No Semantic Highlighting') + endif +endfunction + function! LanguageClient#debugInfo(...) abort let l:params = get(a:000, 0, {}) let l:Callback = get(a:000, 1, v:null) diff --git a/doc/LanguageClient.txt b/doc/LanguageClient.txt index 6314c933..5c915162 100644 --- a/doc/LanguageClient.txt +++ b/doc/LanguageClient.txt @@ -400,6 +400,106 @@ the root of a project is detected using g:LanguageClient_rootMarkers. Default: 1 to display the messages Valid options: 1 | 0 +2.30 g:LanguageClient_semanticHighlightMaps *g:LanguageClient_semanticHighlightMaps* + +String to list/map map. Defines the mapping of semantic highlighting "scopes" to +highlight groups. This depends on the LSP server supporting the proposed +semantic highlighting protocol, see: + +https://github.com/microsoft/language-server-protocol/issues/18 +https://github.com/microsoft/vscode-languageserver-node/issues/368 + + +Like |g:LanguageClient_serverCommands| this is a map where the keys are +filetypes. However each submap has |regexp| keys and highlight group names +as values (see |highlight-groups|). +> + let g:LanguageClient_semanticHighlightMaps = { + \ 'java': { + \ '^entity.name.function.java': 'Function', + \ '^entity.name.type.class.java': 'Type', + \ '^[^:]*entity.name.function.java': 'Function', + \ '^[^:]entity.name.type.class.java': 'Type' + \ } + \ } + +The |regexp| in the keys will be used to match semantic scopes. Then any symbols +that have a semantic scope that matches the key will be highlighted with the +associated highlight group value. Currently there is no defined order if a +semantic scope can match multiple keys, so it is recommended to make the keys +more specific to only match the desired scope(s). + +There are a fixed set of semantic scopes defined by the LSP server on startup. +These can be viewed by calling |LanguageClient_showSemanticScopes| which will +show all the semantic scopes and their currently mapped highlight group for +the currently open buffer's filetype. +> + call LanguageClient_showSemanticScopes() + + == Output from eclipse.jdt.ls == + + Highlight Group: + None + Semantic Scope: + invalid.deprecated.java + meta.class.java + source.java + + Highlight Group: + None + Semantic Scope: + variable.other.autoboxing.java + meta.method.body.java + meta.method.java + meta.class.body.java + meta.class.java + source.java + + ... + +Each semantic scope is a list of strings. They are printed with increasing +indent to make it easier to read. For example the first scope is: +> + ['invalid.deprecated.java', 'meta.class.java', 'source.java'] + +It is currently isn't mapped to any highlight group as indicated by the None. + +Often its more useful to find what semantic scope corresponds to a piece of +text. This can be done by calling |LanguageClient_showCursorSemanticHighlightSymbols| +while hovering over the text of interest. +> + call LanguageClient_showCursorSemanticHighlightSymbols() + +When matching the semantic scopes to keys in |LanguageClient_semanticHighlightMaps|, +the scopes are concatentated using |LanguageClient_semanticScopeSeparator| +which is set to the string |':'| by default. For the previous example the +semantic scope would have this string form using the default separator: +> + invalid.deprecated.java:meta.class.java:source.java + +Here are a couple of example |regexp| keys that can/cannot match this scope: +> + 'meta.class.java' =~ 'invalid.deprecated.java:meta.class.java:source.java' + '^meta.class.java' !~ 'invalid.deprecated.java:meta.class.java:source.java' + '^invalid.deprecated.java' =~ 'invalid.deprecated.java:meta.class.java:source.java' + 'source.java$' =~ 'invalid.deprecated.java:meta.class.java:source.java' + 'meta.class.java:source.java' =~ 'invalid.deprecated.java:meta.class.java:source.java' + 'invalid.deprecated.java:.*:source.java' =~ 'invalid.deprecated.java:meta.class.java:source.java' + +Example configuration for eclipse.jdt.ls: +> + let g:LanguageClient_semanticHighlightMaps = {} + let g:LanguageClient_semanticHighlightMaps['java'] = { + \ '^storage.modifier.static.java:entity.name.function.java': 'JavaStaticMemberFunction', + \ '^meta.definition.variable.java:meta.class.body.java:meta.class.java': 'JavaMemberVariable', + \ '^entity.name.function.java': 'Function', + \ '^[^:]*entity.name.function.java': 'Function', + \ '^[^:]*entity.name.type.class.java': 'Type', + \ } + + highlight! JavaStaticMemberFunction ctermfg=Green cterm=none guifg=Green gui=none + highlight! JavaMemberVariable ctermfg=White cterm=italic guifg=White gui=italic + ============================================================================== 3. Commands *LanguageClientCommands* @@ -678,6 +778,18 @@ Signature: LanguageClient#java_classFileContents(...) Call java/classFileContents. +*LanguageClient_showSemanticScopes* +Signature: LanguageClient_showSemanticScopes(...) + +Get all Semantic Scopes and their associated highlight groups for the current +filetype (filetype of the currently open buffer) and print them. + +*LanguageClient_showCursorSemanticHighlightSymbols* +Signature: LanguageClient_showCursorSemanticHighlightSymbols(...) + +Get the Semantic Scope of the symbol currently under the cursor. +The result gets displayed in a popup. + *LanguageClient#explainErrorAtPoint* Signature: LanguageClient#explainErrorAtPoint(...) diff --git a/plugin/LanguageClient.vim b/plugin/LanguageClient.vim index e878e5e1..9cc12195 100644 --- a/plugin/LanguageClient.vim +++ b/plugin/LanguageClient.vim @@ -2,6 +2,10 @@ if !exists('g:LanguageClient_serverCommands') let g:LanguageClient_serverCommands = {} endif +if !exists('g:LanguageClient_semanticHighlightMaps') + let g:LanguageClient_semanticHighlightMaps = {} +endif + function! LanguageClient_textDocument_hover(...) return call('LanguageClient#textDocument_hover', a:000) endfunction diff --git a/src/language_client.rs b/src/language_client.rs index 5d3d95f5..0a545e0f 100644 --- a/src/language_client.rs +++ b/src/language_client.rs @@ -3,6 +3,7 @@ use crate::vim::Vim; use std::ops::DerefMut; pub struct LanguageClient { + pub version: Arc, pub state_mutex: Arc>, pub clients_mutex: Arc>>>>, } diff --git a/src/language_server_protocol.rs b/src/language_server_protocol.rs index 69302321..e17deb77 100644 --- a/src/language_server_protocol.rs +++ b/src/language_server_protocol.rs @@ -26,6 +26,7 @@ impl LanguageClient { pub fn loop_call(&self, rx: &crossbeam::channel::Receiver) -> Fallible<()> { for call in rx.iter() { let language_client = LanguageClient { + version: self.version.clone(), state_mutex: self.state_mutex.clone(), clients_mutex: self.clients_mutex.clone(), // not sure if useful to clone this }; @@ -113,6 +114,7 @@ impl LanguageClient { .as_ref(), )?; + #[allow(clippy::type_complexity)] let ( diagnosticsSignsMax, diagnostics_max_severity, @@ -120,7 +122,18 @@ impl LanguageClient { selectionUI_autoOpen, use_virtual_text, echo_project_root, - ): (Option, String, Value, u8, UseVirtualText, u8) = self.vim()?.eval( + semanticHighlightMaps, + semanticScopeSeparator, + ): ( + Option, + String, + Value, + u8, + UseVirtualText, + u8, + HashMap>, + String, + ) = self.vim()?.eval( [ "get(g:, 'LanguageClient_diagnosticsSignsMax', v:null)", "get(g:, 'LanguageClient_diagnosticsMaxSeverity', 'Hint')", @@ -128,6 +141,8 @@ impl LanguageClient { "!!s:GetVar('LanguageClient_selectionUI_autoOpen', 1)", "s:useVirtualText()", "!!s:GetVar('LanguageClient_echoProjectRoot', 1)", + "s:GetVar('LanguageClient_semanticHighlightMaps', {})", + "s:GetVar('LanguageClient_semanticScopeSeparator', ':')", ] .as_ref(), )?; @@ -201,8 +216,14 @@ impl LanguageClient { ), }; + let semanticHlUpdateLanguageIds: Vec = + semanticHighlightMaps.keys().cloned().collect(); + self.update(|state| { state.autoStart = autoStart; + state.semanticHighlightMaps = semanticHighlightMaps; + state.semanticScopeSeparator = semanticScopeSeparator; + state.semantic_scope_to_hl_group_table.clear(); state.serverCommands.extend(serverCommands); state.selectionUI = selectionUI; state.selectionUI_autoOpen = selectionUI_autoOpen; @@ -235,6 +256,10 @@ impl LanguageClient { Ok(()) })?; + for languageId in semanticHlUpdateLanguageIds { + self.updateSemanticHighlightTables(&languageId)?; + } + info!("End sync settings"); Ok(()) } @@ -818,6 +843,81 @@ impl LanguageClient { Ok(()) } + fn parseSemanticScopes(&self, languageId: &str, result: &Value) -> Fallible<()> { + info!("Begin parse Semantic Scopes"); + let result: InitializeResult = serde_json::from_value(result.clone())?; + + if let Some(capability) = result.capabilities.semantic_highlighting { + self.update(|state| { + state + .semantic_scopes + .insert(languageId.into(), capability.scopes.unwrap_or_default()); + Ok(()) + })?; + } + + info!("End parse Semantic Scopes"); + Ok(()) + } + + /// Build the Semantic Highlight Lookup Table of + /// + /// ScopeIndex -> Option + fn updateSemanticHighlightTables(&self, languageId: &str) -> Fallible<()> { + info!("Begin updateSemanticHighlightTables"); + let (opt_scopes, opt_hl_map, scopeSeparator) = self.get(|state| { + ( + state.semantic_scopes.get(languageId).cloned(), + state.semanticHighlightMaps.get(languageId).cloned(), + state.semanticScopeSeparator.clone(), + ) + })?; + + if let (Some(semantic_scopes), Some(semanticHighlightMap)) = (opt_scopes, opt_hl_map) { + let mut table: Vec> = Vec::new(); + + for scope_list in semantic_scopes { + // Combine all scopes ["scopeA", "scopeB", ...] -> "scopeA:scopeB:..." + let scope_str = scope_list.iter().join(&scopeSeparator); + + let mut matched = false; + for (scope_regex, hl_group) in &semanticHighlightMap { + let match_expr = format!( + "({} =~ {})", + convert_to_vim_str(&scope_str), + convert_to_vim_str(scope_regex) + ); + + let matches: i32 = self.vim()?.eval(match_expr)?; + + if matches == 1 { + table.push(Some(hl_group.clone())); + matched = true; + break; + } + } + + if !matched { + table.push(None); + } + } + + self.update(|state| { + state + .semantic_scope_to_hl_group_table + .insert(languageId.into(), table); + Ok(()) + })?; + } else { + self.update(|state| { + state.semantic_scope_to_hl_group_table.remove(languageId); + Ok(()) + })?; + } + info!("End updateSemanticHighlightTables"); + Ok(()) + } + fn get_line(&self, path: impl AsRef, line: u64) -> Fallible { let value = self.vim()?.rpcclient.call( "getbufline", @@ -1004,8 +1104,14 @@ impl LanguageClient { let result: Value = self.get_client(&Some(languageId.clone()))?.call( lsp::request::Initialize::METHOD, + #[allow(deprecated)] InitializeParams { + client_info: Some(ClientInfo { + name: "LanguageClient-neovim".into(), + version: Some((*self.version).clone()), + }), process_id: Some(u64::from(std::process::id())), + /* deprecated in lsp types, but can't initialize without it */ root_path: Some(root.clone()), root_uri: Some(root.to_url()?), initialization_options, @@ -1045,10 +1151,16 @@ impl LanguageClient { }), publish_diagnostics: Some(PublishDiagnosticsCapability { related_information: Some(true), + ..PublishDiagnosticsCapability::default() }), code_lens: Some(GenericCapability { dynamic_registration: Some(true), }), + semantic_highlighting_capabilities: Some( + SemanticHighlightingClientCapability { + semantic_highlighting: true, + }, + ), ..TextDocumentClientCapabilities::default() }), workspace: Some(WorkspaceClientCapabilities { @@ -1084,6 +1196,11 @@ impl LanguageClient { error!("{}\n{:?}", message, e); self.vim()?.echoerr(&message)?; } + if let Err(e) = self.parseSemanticScopes(&languageId, &result) { + let message = format!("LanguageClient: failed to parse semantic scopes: {}", e); + error!("{}\n{:?}", message, e); + self.vim()?.echoerr(&message)?; + } Ok(result) } @@ -1092,6 +1209,7 @@ impl LanguageClient { info!("Begin {}", lsp::notification::Initialized::METHOD); let filename = self.vim()?.get_filename(params)?; let languageId = self.vim()?.get_languageId(&filename, params)?; + self.updateSemanticHighlightTables(&languageId)?; self.get_client(&Some(languageId))? .notify(lsp::notification::Initialized::METHOD, InitializedParams {})?; info!("End {}", lsp::notification::Initialized::METHOD); @@ -1232,6 +1350,7 @@ impl LanguageClient { position, }, new_name, + work_done_progress_params: WorkDoneProgressParams::default(), }, )?; @@ -1418,6 +1537,8 @@ impl LanguageClient { diagnostics, only: None, }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), }, )?; @@ -1435,6 +1556,7 @@ impl LanguageClient { diagnostics: None, edit: None, command: Some(command), + ..CodeAction::default() }, CodeActionOrCommand::CodeAction(action) => action, }) @@ -1594,7 +1716,9 @@ impl LanguageClient { tab_size, insert_spaces, properties: HashMap::new(), + ..FormattingOptions::default() }, + work_done_progress_params: WorkDoneProgressParams::default(), }, )?; @@ -1635,6 +1759,7 @@ impl LanguageClient { tab_size, insert_spaces, properties: HashMap::new(), + ..FormattingOptions::default() }, range: Range { start: Position { @@ -1646,6 +1771,7 @@ impl LanguageClient { character: 0, }, }, + work_done_progress_params: WorkDoneProgressParams::default(), }, )?; @@ -1698,7 +1824,11 @@ impl LanguageClient { let query = try_get("query", params)?.unwrap_or_default(); let result = self.get_client(&Some(languageId))?.call( lsp::request::WorkspaceSymbol::METHOD, - WorkspaceSymbolParams { query }, + WorkspaceSymbolParams { + query, + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }, )?; if !self.vim()?.get_handle(params)? { @@ -1773,7 +1903,11 @@ impl LanguageClient { let result = self.get_client(&Some(languageId))?.call( lsp::request::ExecuteCommand::METHOD, - ExecuteCommandParams { command, arguments }, + ExecuteCommandParams { + command, + arguments, + work_done_progress_params: WorkDoneProgressParams::default(), + }, )?; info!("End {}", lsp::request::ExecuteCommand::METHOD); Ok(result) @@ -1871,6 +2005,8 @@ impl LanguageClient { text_document: TextDocumentIdentifier { uri: filename.to_url()?, }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), }; let results: Value = client.call(lsp::request::CodeLensRequest::METHOD, &input)?; @@ -2120,6 +2256,218 @@ impl LanguageClient { Ok(()) } + pub fn textDocument_semanticHighlight(&self, params: &Value) -> Fallible<()> { + info!("Begin {}", lsp::notification::SemanticHighlighting::METHOD); + let mut params: SemanticHighlightingParams = params.clone().to_lsp()?; + + // TODO: Do we need to handle the versioning of the file? + let mut filename = params + .text_document + .uri + .filepath()? + .to_string_lossy() + .into_owned(); + // Workaround bug: remove first '/' in case of '/C:/blabla'. + if filename.chars().nth(0) == Some('/') && filename.chars().nth(2) == Some(':') { + filename.remove(0); + } + // Unify name to avoid mismatch due to case insensitivity. + let filename = filename.canonicalize(); + let languageId = self.vim()?.get_languageId(&filename, &Value::Null)?; + + let opt_hl_table = self.get(|state| { + state + .semantic_scope_to_hl_group_table + .get(&languageId) + .cloned() + })?; + + // Sort lines in ascending order + params.lines.sort_by(|a, b| a.line.cmp(&b.line)); + + // Remove obviously invalid values + while let Some(line_info) = params.lines.first() { + if line_info.line >= 0 { + break; + } else { + warn!( + "Invalid Semantic Highlight Line: {}", + params.lines.remove(0).line + ); + } + } + + let semantic_hl_state = TextDocumentSemanticHighlightState { + last_version: params.text_document.version, + symbols: params.lines, + highlights: None, + }; + + if let Some(hl_table) = opt_hl_table { + let ns_id = self.get_or_create_namespace(&LCNamespace::SemanticHighlight)?; + + let buffer = self.vim()?.get_bufnr(&filename, &Value::Null)?; + + if buffer == -1 { + error!( + "Received Semantic Highlighting for non-open buffer: {}", + filename + ); + return Ok(()); + } + + /* + * Currently servers update entire regions of text at a time or a + * single line so simply clear between the first and last line to + * ensure no highlights are left dangling + */ + let mut clear_region: Option<(u64, u64)> = None; + let mut highlights = Vec::with_capacity(semantic_hl_state.symbols.len()); + + for line in &semantic_hl_state.symbols { + if let Some(tokens) = &line.tokens { + for token in tokens { + if token.length == 0 { + continue; + } + + if let Some(Some(group)) = hl_table.get(token.scope as usize) { + highlights.push(Highlight { + line: line.line as u64, + character_start: token.character as u64, + character_end: token.character as u64 + token.length as u64, + group: group.clone(), + text: String::new(), + }); + } + } + + match clear_region { + Some((begin, _)) => { + clear_region = Some((begin, line.line as u64 + 1)); + } + None => { + clear_region = Some((line.line as u64, line.line as u64 + 1)); + } + } + } + } + + info!( + "Semantic Highlighting Region [{}, {}]:", + semantic_hl_state + .symbols + .first() + .map_or(-1, |h| h.line as i64), + semantic_hl_state + .symbols + .last() + .map_or(-1, |h| h.line as i64) + ); + + info!( + "Semantic Highlighting Region (Parsed) [{}, {}]:", + highlights.first().map_or(-1, |h| h.line as i64), + highlights.last().map_or(-1, |h| h.line as i64) + ); + + let mut clears = Vec::new(); + if let Some((begin, end)) = clear_region { + clears.push(ClearNamespace { + line_start: begin, + line_end: end, + }); + } + + let mut num_semantic_hls = 0; + let num_new_semantic_hls = highlights.len(); + + self.update(|state| { + state.vim.rpcclient.notify( + "s:ApplySemanticHighlights", + json!([buffer, ns_id, clears, highlights]), + )?; + + let old_semantic_hl_state = state + .semantic_highlights + .insert(languageId.clone(), semantic_hl_state); + + let semantic_hl_state = state.semantic_highlights.get_mut(&languageId).unwrap(); + + let mut combined_hls = Vec::with_capacity(highlights.len()); + + let mut existing_hls = old_semantic_hl_state + .map_or(Vec::new(), |hl_state| { + hl_state.highlights.unwrap_or_default() + }) + .into_iter() + .peekable(); + + let mut new_hls = highlights.into_iter().peekable(); + + // Incrementally update the highlighting + loop { + match (existing_hls.peek(), new_hls.peek()) { + (Some(existing_hl), Some(new_hl)) => { + use std::cmp::Ordering; + + match existing_hl.line.cmp(&new_hl.line) { + Ordering::Less => { + if clear_region.unwrap_or((0, 0)).0 <= existing_hl.line + && existing_hl.line < clear_region.unwrap_or((0, 0)).1 + { + // within clear region, this highlight gets cleared + existing_hls.next(); + } else { + combined_hls + .push(existing_hls.next().expect("unreachable")); + } + } + Ordering::Greater => { + combined_hls.push(new_hls.next().expect("unreachable")); + } + Ordering::Equal => { + // existing highlight on same line as new, it gets cleared + existing_hls.next(); + } + } + } + (Some(_), None) => { + combined_hls.push(existing_hls.next().expect("unreachable")); + } + (None, Some(_)) => { + combined_hls.push(new_hls.next().expect("unreachable")); + } + (None, None) => { + break; + } + } + } + + num_semantic_hls = combined_hls.len(); + + semantic_hl_state.highlights = Some(combined_hls); + + Ok(()) + })?; + + info!( + "Applied Semantic Highlighting for {} Symbols ({} new)", + num_semantic_hls, num_new_semantic_hls + ) + } else { + self.update(|state| { + state + .semantic_highlights + .insert(languageId.clone(), semantic_hl_state); + Ok(()) + })?; + } + + info!("End {}", lsp::notification::SemanticHighlighting::METHOD); + Ok(()) + } + pub fn window_logMessage(&self, params: &Value) -> Fallible<()> { info!("Begin {}", lsp::notification::LogMessage::METHOD); let params: LogMessageParams = params.clone().to_lsp()?; @@ -2701,7 +3049,7 @@ impl LanguageClient { } if self.get(|state| state.is_nvim)? { - let namespace_id = self.get_or_create_namespace()?; + let namespace_id = self.get_or_create_namespace(&LCNamespace::VirtualText)?; self.vim()?.set_virtual_texts( bufnr, namespace_id, @@ -2864,6 +3212,80 @@ impl LanguageClient { Ok(()) } + pub fn languageClient_semanticScopes(&self, params: &Value) -> Fallible { + let filename = self.vim()?.get_filename(params)?; + let languageId = self.vim()?.get_languageId(&filename, params)?; + + let (scopes, mut scope_mapping) = self.get(|state| { + ( + state + .semantic_scopes + .get(&languageId) + .cloned() + .unwrap_or_default(), + state + .semantic_scope_to_hl_group_table + .get(&languageId) + .cloned() + .unwrap_or_default(), + ) + })?; + + let mut semantic_scopes = Vec::new(); + + // If the user has not set up highlighting yet the table does not exist + if scopes.len() > scope_mapping.len() { + scope_mapping.resize(scopes.len(), None); + } + + for (scope, opt_hl_group) in scopes.iter().zip(scope_mapping.iter()) { + if let Some(hl_group) = opt_hl_group { + semantic_scopes.push(json!({ + "scope": scope, + "hl_group": hl_group, + })); + } else { + semantic_scopes.push(json!({ + "scope": scope, + "hl_group": "None", + })); + } + } + + Ok(json!(semantic_scopes)) + } + + pub fn languageClient_semanticHlSyms(&self, params: &Value) -> Fallible { + let filename = self.vim()?.get_filename(params)?; + let languageId = self.vim()?.get_languageId(&filename, params)?; + + let (opt_scopes, opt_hl_state) = self.get(|state| { + ( + state.semantic_scopes.get(&languageId).cloned(), + state.semantic_highlights.get(&languageId).cloned(), + ) + })?; + + if let (Some(scopes), Some(hl_state)) = (opt_scopes, opt_hl_state) { + let mut symbols = Vec::new(); + + for sym in hl_state.symbols { + for token in sym.tokens.unwrap_or_default() { + symbols.push(json!({ + "line": sym.line as u64, + "character_start": token.character as u64, + "character_end": token.character as u64 + token.length as u64, + "scope": scopes.get(token.scope as usize).cloned().unwrap_or_default() + })); + } + } + + Ok(json!(symbols)) + } else { + Ok(json!([])) + } + } + pub fn NCM_refresh(&self, params: &Value) -> Fallible { info!("Begin {}", REQUEST__NCMRefresh); let params: NCMRefreshParams = serde_json::from_value(rpc::to_value(params.clone())?)?; diff --git a/src/main.rs b/src/main.rs index 124c8038..1cd4df78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,7 @@ fn main() -> Fallible<()> { let (tx, rx) = crossbeam::channel::unbounded(); let language_client = language_client::LanguageClient { + version: Arc::new(version), state_mutex: Arc::new(Mutex::new(State::new(tx)?)), clients_mutex: Arc::new(Mutex::new(HashMap::new())), }; diff --git a/src/rpchandler.rs b/src/rpchandler.rs index b186994a..6d27bdb4 100644 --- a/src/rpchandler.rs +++ b/src/rpchandler.rs @@ -100,6 +100,8 @@ impl LanguageClient { REQUEST__ClassFileContents => self.java_classFileContents(¶ms), REQUEST__DebugInfo => self.debug_info(¶ms), REQUEST__CodeLensAction => self.languageClient_handleCodeLensAction(¶ms), + REQUEST__SemanticScopes => self.languageClient_semanticScopes(¶ms), + REQUEST__ShowSemanticHighlightSymbols => self.languageClient_semanticHlSyms(¶ms), _ => { let languageId_target = if languageId.is_some() { @@ -156,6 +158,9 @@ impl LanguageClient { lsp::notification::PublishDiagnostics::METHOD => { self.textDocument_publishDiagnostics(¶ms)? } + lsp::notification::SemanticHighlighting::METHOD => { + self.textDocument_semanticHighlight(¶ms)? + } lsp::notification::LogMessage::METHOD => self.window_logMessage(¶ms)?, lsp::notification::ShowMessage::METHOD => self.window_showMessage(¶ms)?, lsp::notification::Exit::METHOD => self.exit(¶ms)?, diff --git a/src/types.rs b/src/types.rs index 8c7d975e..f5e36d3c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -33,6 +33,9 @@ pub const REQUEST__ExplainErrorAtPoint: &str = "languageClient/explainErrorAtPoi pub const REQUEST__FindLocations: &str = "languageClient/findLocations"; pub const REQUEST__DebugInfo: &str = "languageClient/debugInfo"; pub const REQUEST__CodeLensAction: &str = "LanguageClient/handleCodeLensAction"; +pub const REQUEST__SemanticScopes: &str = "languageClient/semanticScopes"; +pub const REQUEST__ShowSemanticHighlightSymbols: &str = + "languageClient/showSemanticHighlightSymbols"; pub const NOTIFICATION__HandleBufNewFile: &str = "languageClient/handleBufNewFile"; pub const NOTIFICATION__HandleFileType: &str = "languageClient/handleFileType"; pub const NOTIFICATION__HandleTextChanged: &str = "languageClient/handleTextChanged"; @@ -120,6 +123,10 @@ pub struct State { pub roots: HashMap, pub text_documents: HashMap, pub text_documents_metadata: HashMap, + pub semantic_scopes: HashMap>>, + pub semantic_scope_to_hl_group_table: HashMap>>, + // filename => semantic highlight state + pub semantic_highlights: HashMap, // filename => diagnostics. pub diagnostics: HashMap>, // filename => codeLens. @@ -129,7 +136,7 @@ pub struct State { pub sign_next_id: u64, /// Active signs. pub signs: HashMap>, - pub namespace_id: Option, + pub namespace_ids: HashMap, pub highlight_source: Option, pub highlights: HashMap>, pub highlights_placed: HashMap>, @@ -149,6 +156,9 @@ pub struct State { // User settings. pub serverCommands: HashMap>, + // languageId => (scope_regex => highlight group) + pub semanticHighlightMaps: HashMap>, + pub semanticScopeSeparator: String, pub autoStart: bool, pub selectionUI: SelectionUI, pub selectionUI_autoOpen: bool, @@ -204,12 +214,15 @@ impl State { roots: HashMap::new(), text_documents: HashMap::new(), text_documents_metadata: HashMap::new(), + semantic_scopes: HashMap::new(), + semantic_scope_to_hl_group_table: HashMap::new(), + semantic_highlights: HashMap::new(), code_lens: HashMap::new(), diagnostics: HashMap::new(), line_diagnostics: HashMap::new(), sign_next_id: 75_000, signs: HashMap::new(), - namespace_id: None, + namespace_ids: HashMap::new(), highlight_source: None, highlights: HashMap::new(), highlights_placed: HashMap::new(), @@ -225,6 +238,8 @@ impl State { stashed_codeAction_actions: vec![], serverCommands: HashMap::new(), + semanticHighlightMaps: HashMap::new(), + semanticScopeSeparator: ":".into(), autoStart: true, selectionUI: SelectionUI::LocationList, selectionUI_autoOpen: true, @@ -280,6 +295,20 @@ impl FromStr for SelectionUI { } } +pub enum LCNamespace { + VirtualText, + SemanticHighlight, +} + +impl LCNamespace { + pub fn name(&self) -> String { + match self { + LCNamespace::VirtualText => "LanguageClient_VirtualText".into(), + LCNamespace::SemanticHighlight => "LanguageClient_SemanticHighlight".into(), + } + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum HoverPreviewOption { Always, @@ -422,6 +451,13 @@ impl DocumentHighlightDisplay { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TextDocumentSemanticHighlightState { + pub last_version: Option, + pub symbols: Vec, + pub highlights: Option>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Highlight { pub line: u64, @@ -438,6 +474,12 @@ impl PartialEq for Highlight { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClearNamespace { + pub line_start: u64, + pub line_end: u64, +} + #[derive(Debug, Serialize, Deserialize)] pub struct QuickfixEntry { pub filename: String, diff --git a/src/utils.rs b/src/utils.rs index 0fd3a7fc..61c45fee 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -464,3 +464,39 @@ pub fn decode_parameterLabel( } } } + +/// Given a string, convert it into a string for vimscript +/// The string gets surrounded by single quotes. +/// +/// Existing single quotes will get escaped by inserting +/// another single quote in place. +/// +/// E.g. +/// abcdefg -> 'abcdefg' +/// abdcef'g -> 'abcdef''g' +pub fn convert_to_vim_str(s: &str) -> String { + let mut vs = String::with_capacity(s.len()); + + vs.push('\''); + + for i in s.chars() { + if i == '\'' { + vs.push(i); + } + + vs.push(i); + } + + vs.push('\''); + + vs +} + +#[test] +fn test_convert_to_vim_str() { + assert_eq!(convert_to_vim_str("abcdefg"), "'abcdefg'"); + assert_eq!(convert_to_vim_str("'abcdefg"), "'''abcdefg'"); + assert_eq!(convert_to_vim_str("'x'x'x'x'"), "'''x''x''x''x'''"); + assert_eq!(convert_to_vim_str("xyz'''ffff"), "'xyz''''''ffff'"); + assert_eq!(convert_to_vim_str("'''"), "''''''''"); +} diff --git a/src/vimext.rs b/src/vimext.rs index e94e2ac9..553ada9f 100644 --- a/src/vimext.rs +++ b/src/vimext.rs @@ -1,14 +1,17 @@ use crate::language_client::LanguageClient; +use crate::types::LCNamespace; use failure::Fallible; impl LanguageClient { - pub fn get_or_create_namespace(&self) -> Fallible { - if let Some(namespace_id) = self.get(|state| state.namespace_id)? { + pub fn get_or_create_namespace(&self, ns: &LCNamespace) -> Fallible { + let ns_name = ns.name(); + + if let Some(namespace_id) = self.get(|state| state.namespace_ids.get(&ns_name).cloned())? { Ok(namespace_id) } else { - let namespace_id = self.vim()?.create_namespace("LanguageClient")?; + let namespace_id = self.vim()?.create_namespace(&ns_name)?; self.update(|state| { - state.namespace_id = Some(namespace_id); + state.namespace_ids.insert(ns_name, namespace_id); Ok(()) })?; Ok(namespace_id)