Skip to content

Commit 4288360

Browse files
committed
Add virtual text support.
Close #664.
1 parent b8a6898 commit 4288360

File tree

6 files changed

+149
-8
lines changed

6 files changed

+149
-8
lines changed

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
- Replace echodoc functionality.
55
- Create Context to lazy load var/state from vim.
66
- Async/await rust.
7+
- Benchmark batched sign operations to see if optimization is really needed.

autoload/LanguageClient.vim

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ function! s:hasSnippetSupport() abort
115115
return 0
116116
endfunction
117117

118+
function! s:useVirtualText() abort
119+
let l:use = s:GetVar('LanguageClient_useVirtualText')
120+
if l:use !=# v:null
121+
return !!l:use
122+
endif
123+
124+
return exists('*nvim_buf_set_virtual_text')
125+
endfunction
126+
118127
function! s:IsTrue(v) abort
119128
if type(a:v) ==# type(0)
120129
return a:v ==# 0 ? v:false : v:true
@@ -134,6 +143,20 @@ function! s:Bufnames() abort
134143
return map(filter(range(0,bufnr('$')), 'buflisted(v:val)'), 'fnamemodify(bufname(v:val), '':p'')')
135144
endfunction
136145

146+
function! s:set_virtual_texts(buf_id, ns_id, line_start, line_end, virtual_texts) abort
147+
" VirtualText: map with keys line, text and hl_group.
148+
149+
if !exists('*nvim_buf_set_virtual_text')
150+
return
151+
endif
152+
153+
call nvim_buf_clear_namespace(a:buf_id, a:ns_id, a:line_start, a:line_end)
154+
155+
for vt in a:virtual_texts
156+
call nvim_buf_set_virtual_text(a:buf_id, a:ns_id, vt['line'], [[vt['text'], vt['hl_group']]], {})
157+
endfor
158+
endfunction
159+
137160
function! s:getInput(prompt, default) abort
138161
call inputsave()
139162
let l:input = input(a:prompt, a:default)
@@ -199,7 +222,7 @@ function! s:AddHighlights(source, highlights) abort
199222
endfunction
200223

201224
" Get an variable value.
202-
" First try buffer local, then global, then default, then v:null.
225+
" Get variable from uffer local, or else global, or else default, or else v:null.
203226
function! s:GetVar(...) abort
204227
let name = a:1
205228

doc/LanguageClient.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,28 @@ Default: >
100100
"texthl": "ALEError",
101101
"signText": "✖",
102102
"signTexthl": "ALEErrorSign",
103+
"virtualTexthl": "Error",
103104
},
104105
2: {
105106
"name": "Warning",
106107
"texthl": "ALEWarning",
107108
"signText": "⚠",
108109
"signTexthl": "ALEWarningSign",
110+
"virtualTexthl": "Todo",
109111
},
110112
3: {
111113
"name": "Information",
112114
"texthl": "ALEInfo",
113115
"signText": "ℹ",
114116
"signTexthl": "ALEInfoSign",
117+
"virtualTexthl": "Todo",
115118
},
116119
4: {
117120
"name": "Hint",
118121
"texthl": "ALEInfo",
119122
"signText": "➤",
120123
"signTexthl": "ALEInfoSign",
124+
"virtualTexthl": "Todo",
121125
},
122126
}
123127
@@ -336,6 +340,13 @@ Default: >
336340
},
337341
}
338342
343+
2.22 g:LanguageClient_useVirtualText *g:LanguageClient_useVirtualText*
344+
345+
Specify whether to use virtual text to display diagnostics.
346+
347+
Default: 1 whenever virtual text is supported.
348+
Valid Options: 1 | 0
349+
339350
==============================================================================
340351
3. Commands *LanguageClientCommands*
341352

src/language_server_protocol.rs

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use crate::vim::VirtualText;
23

34
use crate::language_client::LanguageClient;
45
use crate::lsp::notification::Notification;
@@ -151,13 +152,17 @@ impl LanguageClient {
151152
.as_ref(),
152153
)?;
153154

154-
let selectionUI_autoOpen: u64 =
155-
self.eval("get(g:, 'LanguageClient_selectionUI_autoOpen', 1)")?;
156-
157-
let (diagnosticsSignsMax, documentHighlightDisplay): (Option<u64>, Value) = self.eval(
155+
let (diagnosticsSignsMax, documentHighlightDisplay, selectionUI_autoOpen, use_virtual_text): (
156+
Option<u64>,
157+
Value,
158+
u8,
159+
u8,
160+
) = self.eval(
158161
[
159162
"get(g:, 'LanguageClient_diagnosticsSignsMax', v:null)",
160163
"get(g:, 'LanguageClient_documentHighlightDisplay', {})",
164+
"!!s:GetVar('LanguageClient_selectionUI_autoOpen', 1)",
165+
"s:useVirtualText()",
161166
]
162167
.as_ref(),
163168
)?;
@@ -243,6 +248,7 @@ impl LanguageClient {
243248
state.wait_output_timeout = wait_output_timeout;
244249
state.hoverPreview = hoverPreview;
245250
state.completionPreferTextEdit = completionPreferTextEdit;
251+
state.use_virtual_text = use_virtual_text == 1;
246252
state.loggingFile = loggingFile;
247253
state.loggingLevel = loggingLevel;
248254
state.serverStderr = serverStderr;
@@ -1883,7 +1889,20 @@ impl LanguageClient {
18831889
// Unify name to avoid mismatch due to case insensitivity.
18841890
let filename = filename.canonicalize();
18851891

1886-
let diagnostics = params.diagnostics;
1892+
let mut diagnostics = params.diagnostics;
1893+
diagnostics.sort_by_key(
1894+
// First sort by line.
1895+
// Then severity descendingly. Error should come last since when processing item comes
1896+
// later will override its precedance.
1897+
// Then by character descendingly.
1898+
|diagnostic| {
1899+
(
1900+
diagnostic.range.start.line,
1901+
-(diagnostic.severity.unwrap_or(DiagnosticSeverity::Hint) as i8),
1902+
-(diagnostic.range.start.line as i64),
1903+
)
1904+
},
1905+
);
18871906

18881907
self.update(|state| {
18891908
state
@@ -2258,8 +2277,13 @@ impl LanguageClient {
22582277

22592278
pub fn languageClient_handleCursorMoved(&self, params: &Value) -> Fallible<()> {
22602279
info!("Begin {}", NOTIFICATION__HandleCursorMoved);
2261-
let (languageId, filename, line): (String, String, u64) = self.gather_args(
2262-
&[VimVar::LanguageId, VimVar::Filename, VimVar::Line],
2280+
let (languageId, filename, bufnr, line): (String, String, i64, u64) = self.gather_args(
2281+
&[
2282+
VimVar::LanguageId,
2283+
VimVar::Filename,
2284+
VimVar::Bufnr,
2285+
VimVar::Line,
2286+
],
22632287
params,
22642288
)?;
22652289
if !self.get(|state| state.serverCommands.contains_key(&languageId))? {
@@ -2379,6 +2403,53 @@ impl LanguageClient {
23792403
.notify("s:AddHighlights", json!([source, highlights]))?;
23802404
}
23812405

2406+
if self.get(|state| state.use_virtual_text)? {
2407+
let namespace_id = if let Some(namespace_id) = self.get(|state| state.namespace_id)? {
2408+
namespace_id
2409+
} else {
2410+
let namespace_id = self.create_namespace("LanguageClient")?;
2411+
self.update(|state| {
2412+
state.namespace_id = Some(namespace_id);
2413+
Ok(())
2414+
})?;
2415+
namespace_id
2416+
};
2417+
let mut virtual_texts = vec![];
2418+
self.update(|state| {
2419+
if let Some(diagnostics) = state.diagnostics.get(&filename) {
2420+
for diagnostic in diagnostics {
2421+
if diagnostic.range.start.line >= visible_line_start
2422+
&& diagnostic.range.start.line <= visible_line_end
2423+
{
2424+
virtual_texts.push(VirtualText {
2425+
line: diagnostic.range.start.line,
2426+
text: diagnostic.message.clone(),
2427+
hl_group: state
2428+
.diagnosticsDisplay
2429+
.get(
2430+
&diagnostic
2431+
.severity
2432+
.unwrap_or(DiagnosticSeverity::Hint)
2433+
.to_int()?,
2434+
)
2435+
.ok_or_else(|| err_msg("Failed to get display"))?
2436+
.virtualTexthl
2437+
.clone(),
2438+
});
2439+
}
2440+
}
2441+
}
2442+
Ok(())
2443+
})?;
2444+
self.set_virtual_texts(
2445+
bufnr,
2446+
namespace_id,
2447+
visible_line_start,
2448+
visible_line_end,
2449+
&virtual_texts,
2450+
)?;
2451+
}
2452+
23822453
info!("End {}", NOTIFICATION__HandleCursorMoved);
23832454
Ok(())
23842455
}

src/types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,13 @@ pub struct State {
104104
pub text_documents: HashMap<String, TextDocumentItem>,
105105
pub text_documents_metadata: HashMap<String, TextDocumentItemMetadata>,
106106
// filename => diagnostics.
107+
// TODO: convert to filename => line => diagnostics, where line => diagnostics is a TreeMap.
107108
pub diagnostics: HashMap<String, Vec<Diagnostic>>,
108109
#[serde(skip_serializing)]
109110
pub line_diagnostics: HashMap<(String, u64), String>,
110111
pub signs: HashMap<String, Vec<Sign>>,
111112
pub signs_placed: HashMap<String, Vec<Sign>>,
113+
pub namespace_id: Option<i64>,
112114
pub highlight_source: Option<u64>,
113115
pub highlights: HashMap<String, Vec<Highlight>>,
114116
pub highlights_placed: HashMap<String, Vec<Highlight>>,
@@ -145,6 +147,7 @@ pub struct State {
145147
pub wait_output_timeout: Duration,
146148
pub hoverPreview: HoverPreviewOption,
147149
pub completionPreferTextEdit: bool,
150+
pub use_virtual_text: bool,
148151

149152
pub loggingFile: Option<String>,
150153
pub loggingLevel: log::LevelFilter,
@@ -182,6 +185,7 @@ impl State {
182185
line_diagnostics: HashMap::new(),
183186
signs: HashMap::new(),
184187
signs_placed: HashMap::new(),
188+
namespace_id: None,
185189
highlight_source: None,
186190
highlights: HashMap::new(),
187191
highlights_placed: HashMap::new(),
@@ -214,6 +218,7 @@ impl State {
214218
wait_output_timeout: Duration::from_secs(10),
215219
hoverPreview: HoverPreviewOption::default(),
216220
completionPreferTextEdit: false,
221+
use_virtual_text: true,
217222
loggingFile: None,
218223
loggingLevel: log::LevelFilter::Warn,
219224
serverStderr: None,
@@ -307,6 +312,7 @@ pub struct DiagnosticsDisplay {
307312
pub texthl: String,
308313
pub signText: String,
309314
pub signTexthl: String,
315+
pub virtualTexthl: String,
310316
}
311317

312318
impl DiagnosticsDisplay {
@@ -319,6 +325,7 @@ impl DiagnosticsDisplay {
319325
texthl: "ALEError".to_owned(),
320326
signText: "✖".to_owned(),
321327
signTexthl: "ALEErrorSign".to_owned(),
328+
virtualTexthl: "Error".to_owned(),
322329
},
323330
);
324331
map.insert(
@@ -328,6 +335,7 @@ impl DiagnosticsDisplay {
328335
texthl: "ALEWarning".to_owned(),
329336
signText: "⚠".to_owned(),
330337
signTexthl: "ALEWarningSign".to_owned(),
338+
virtualTexthl: "Todo".to_owned(),
331339
},
332340
);
333341
map.insert(
@@ -337,6 +345,7 @@ impl DiagnosticsDisplay {
337345
texthl: "ALEInfo".to_owned(),
338346
signText: "ℹ".to_owned(),
339347
signTexthl: "ALEInfoSign".to_owned(),
348+
virtualTexthl: "Todo".to_owned(),
340349
},
341350
);
342351
map.insert(
@@ -346,6 +355,7 @@ impl DiagnosticsDisplay {
346355
texthl: "ALEInfo".to_owned(),
347356
signText: "➤".to_owned(),
348357
signTexthl: "ALEInfoSign".to_owned(),
358+
virtualTexthl: "Todo".to_owned(),
349359
},
350360
);
351361
map

src/vim.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ use super::*;
22
use crate::language_client::LanguageClient;
33
use crate::rpcclient::RpcClient;
44

5+
#[derive(Debug, Eq, PartialEq, Serialize)]
6+
pub struct VirtualText {
7+
pub line: u64,
8+
pub text: String,
9+
pub hl_group: String,
10+
}
11+
512
impl LanguageClient {
613
/////// Vim wrappers ///////
714

@@ -100,6 +107,24 @@ impl LanguageClient {
100107
self.vim()?.notify("setloclist", parms)?;
101108
Ok(())
102109
}
110+
111+
pub fn create_namespace(&self, name: &str) -> Fallible<i64> {
112+
self.vim()?.call("nvim_create_namespace", [name])
113+
}
114+
115+
pub fn set_virtual_texts(
116+
&self,
117+
buf_id: i64,
118+
ns_id: i64,
119+
line_start: u64,
120+
line_end: u64,
121+
virtual_texts: &[VirtualText],
122+
) -> Fallible<i64> {
123+
self.vim()?.call(
124+
"s:set_virtual_texts",
125+
json!([buf_id, ns_id, line_start, line_end, virtual_texts]),
126+
)
127+
}
103128
}
104129

105130
#[derive(Debug, Serialize, Deserialize)]

0 commit comments

Comments
 (0)