From 070dcc00f1bab9f63dd66fe7e44e197fa1057d36 Mon Sep 17 00:00:00 2001 From: Liu-Cheng Xu Date: Thu, 29 Feb 2024 22:29:42 +0800 Subject: [PATCH] Overhaul plugin actions and ensure the display highlights are cleared on empty query (#1049) * Clear display highlights when no matches found * Remove unused deps * Fix custom language server config * Nits * Add params to goto_() * Introduce AutocmdEventType::parse() so that no missing autocmd event again * fix buf detach params * . * Nits * Add language-servers = ["clangd"] for language cpp * Ignore coc-explorer in lsp plugin * fix didChange params for Vim * [vim] reduce unnecesary highlight redraw by removing all from prop_remove() * Remove unwrap() in tree_sitter ConfigInner * Make asset file downloading generic In order to prepare the support for downloading various asset files from GitHub release, pretty useful for LSP plugin. * Refactor upgrade * Allow overriding the default language config * Separate out winbar module * Introduec diagnostics plugin * Distinguish the diagnostic error and warn highlight * Update CHANGELOG.md * Update CHANGELOG.md * Move echoDiagnostics and echoDiagnosticsAtCursor to diagnostics plugin * Rename to diagnostics.buffer and diagnostics.cursor * Support opening lsp locations (refs, defs, etc) in fuzzy picker * Add docs to config() * Update config.md --- CHANGELOG.md | 7 +- Cargo.lock | 17 +- autoload/clap/highlighter.vim | 18 +- autoload/clap/picker.vim | 1 + autoload/clap/plugin/cursorword.vim | 2 +- autoload/clap/plugin/linter.vim | 25 +- autoload/clap/plugin/lsp.vim | 46 +-- autoload/clap/popup/move_manager.vim | 4 +- crates/cli/Cargo.toml | 3 - crates/code_tools/Cargo.toml | 2 - crates/code_tools/src/language.rs | 20 +- crates/filter/Cargo.toml | 5 +- crates/maple_config/Cargo.toml | 1 + crates/maple_config/doc_gen/Cargo.toml | 2 - .../maple_config/doc_gen/default_config.toml | 16 +- crates/maple_config/src/lib.rs | 152 +++++-- crates/maple_core/Cargo.toml | 1 - crates/maple_core/src/process/subprocess.rs | 2 +- .../src/stdio_server/diagnostics_worker.rs | 10 +- crates/maple_core/src/stdio_server/input.rs | 20 +- crates/maple_core/src/stdio_server/mod.rs | 23 +- .../src/stdio_server/plugin/ctags.rs | 120 +----- .../src/stdio_server/plugin/cursorword.rs | 2 +- .../src/stdio_server/plugin/diagnostics.rs | 102 +++++ .../maple_core/src/stdio_server/plugin/git.rs | 19 +- .../src/stdio_server/plugin/linter.rs | 62 --- .../maple_core/src/stdio_server/plugin/lsp.rs | 370 ++++++++++++------ .../src/stdio_server/plugin/lsp/handler.rs | 7 +- .../src/stdio_server/plugin/markdown.rs | 2 +- .../maple_core/src/stdio_server/plugin/mod.rs | 2 + .../src/stdio_server/plugin/syntax.rs | 32 +- .../src/stdio_server/plugin/system.rs | 12 +- .../src/stdio_server/provider/impls/grep.rs | 2 - .../src/stdio_server/provider/impls/lsp.rs | 76 ++++ crates/maple_core/src/stdio_server/winbar.rs | 117 ++++++ crates/maple_core/src/types.rs | 15 + crates/maple_derive/src/impls/clap_plugin.rs | 36 +- crates/sublime_syntax/Cargo.toml | 2 - crates/tree_sitter/Cargo.toml | 1 + crates/tree_sitter/src/language.rs | 49 ++- crates/types/src/lib.rs | 10 + crates/upgrade/Cargo.toml | 1 - crates/upgrade/src/github.rs | 129 +++--- crates/upgrade/src/lib.rs | 245 +----------- crates/upgrade/src/maple_upgrade.rs | 276 +++++++++++++ crates/utils/Cargo.toml | 2 - crates/xtask/src/release.rs | 1 + docs/src/plugins/config.md | 16 +- docs/src/plugins/plugins.md | 4 + languages.toml | 12 +- plugin/clap.vim | 4 +- 51 files changed, 1253 insertions(+), 852 deletions(-) create mode 100644 crates/maple_core/src/stdio_server/plugin/diagnostics.rs create mode 100644 crates/maple_core/src/stdio_server/winbar.rs create mode 100644 crates/upgrade/src/maple_upgrade.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index df62783b2..917637ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## [unreleased] -- Change the syntax of plugin actions from `plugin/action` to `plugin.action` for better compatibility with other tools. +### Plugins + +- Change the naming convention of plugin action from `plugin/foo-action` to `plugin.fooAction` for the compatibility with tools like coc.nvim. +- Use different highlight groups for the span of error and warn diagnostics. +- Added diagnostics plugin in order to conveniently inspect the collected diagnostics from both the linter and lsp plugin. + Now you should use `:ClapAction diagnostics.firstError` instead of `:ClapAction linter.firstError` to jump to the position of first error. ### Internal diff --git a/Cargo.lock b/Cargo.lock index 8062fb88a..2ab119421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,17 +380,14 @@ dependencies = [ "clap", "criterion", "filter", - "futures", "icon", "itertools 0.10.5", "maple_config", "maple_core", "matcher", "num_cpus", - "pattern", "printer", "rayon", - "regex", "serde", "serde_json", "subprocess", @@ -416,12 +413,10 @@ dependencies = [ name = "code_tools" version = "0.1.0" dependencies = [ - "async-trait", "cargo_metadata", "maple_config", "maple_lsp", "once_cell", - "parking_lot", "paths", "regex", "serde", @@ -688,8 +683,6 @@ dependencies = [ "inflections", "itertools 0.12.1", "maple_config", - "maple_core", - "proc-macro2", "quote", "syn 1.0.109", "toml", @@ -759,16 +752,13 @@ dependencies = [ "icon", "matcher", "parking_lot", - "pattern", "printer", "rayon", - "serde", "serde_json", "subprocess", "thiserror", "tracing", "types", - "utils", ] [[package]] @@ -1454,6 +1444,7 @@ dependencies = [ "once_cell", "paths", "serde", + "serde_json", "toml", "types", ] @@ -1464,7 +1455,6 @@ version = "0.1.0" dependencies = [ "async-trait", "base64 0.13.1", - "bytecount", "chrono", "chrono-humanize", "clap", @@ -2393,11 +2383,9 @@ name = "sublime_syntax" version = "0.1.0" dependencies = [ "colors-transform", - "once_cell", "rgb2ansi256", "serde", "syntect", - "tracing", "utils", ] @@ -2888,6 +2876,7 @@ dependencies = [ "rand", "serde", "toml", + "tracing", "tree-sitter", "tree-sitter-bash", "tree-sitter-c", @@ -2959,7 +2948,6 @@ dependencies = [ "indicatif", "reqwest", "serde", - "serde_json", "tokio", ] @@ -2988,7 +2976,6 @@ dependencies = [ "bytecount", "memchr", "simdutf8", - "types", ] [[package]] diff --git a/autoload/clap/highlighter.vim b/autoload/clap/highlighter.vim index 2fdc9f781..52045f050 100644 --- a/autoload/clap/highlighter.vim +++ b/autoload/clap/highlighter.vim @@ -9,7 +9,7 @@ let s:default_priority = 10 if has('nvim') let s:tree_sitter_ns_id = nvim_create_namespace('clap_tree_sitter_highlight') else - let s:ts_types = [] + let s:ts_prop_types = [] endif if has('nvim') @@ -163,8 +163,8 @@ endfunction function! clap#highlighter#disable_tree_sitter(bufnr) abort if has('nvim') call nvim_buf_clear_namespace(a:bufnr, s:tree_sitter_ns_id, 0, -1) - elseif !empty(s:ts_types) - call prop_remove({ 'types': s:ts_types, 'all': v:true, 'bufnr': a:bufnr } ) + elseif !empty(s:ts_prop_types) + call prop_remove({ 'types': s:ts_prop_types, 'all': v:true, 'bufnr': a:bufnr } ) endif endfunction @@ -178,13 +178,13 @@ function! clap#highlighter#add_ts_highlights(bufnr, to_replace_line_ranges, high call nvim_buf_clear_namespace(a:bufnr, s:tree_sitter_ns_id, start, end) endfor endif - elseif !empty(s:ts_types) + elseif !empty(s:ts_prop_types) if empty(a:to_replace_line_ranges) - call prop_remove({ 'types': s:ts_types, 'all': v:true, 'bufnr': a:bufnr } ) + call prop_remove({ 'types': s:ts_prop_types, 'all': v:true, 'bufnr': a:bufnr } ) else for [start, end] in a:to_replace_line_ranges " start is 0-based - call prop_remove({ 'types': s:ts_types, 'all': v:true, 'bufnr': a:bufnr }, start+1, end) + call prop_remove({ 'types': s:ts_prop_types, 'bufnr': a:bufnr }, start+1, end) endfor endif endif @@ -192,9 +192,9 @@ function! clap#highlighter#add_ts_highlights(bufnr, to_replace_line_ranges, high for [line_number, highlights] in a:highlights for [column_start, length, group_name] in highlights if !has('nvim') - if index(s:ts_types, group_name) == -1 - call add(s:ts_types, group_name) - call prop_type_add(group_name, {'highlight': group_name}) + if index(s:ts_prop_types, group_name) == -1 + call add(s:ts_prop_types, group_name) + call prop_type_add(group_name, {'highlight': group_name}) endif endif call s:add_ts_highlight_at(a:bufnr, line_number, column_start, length, group_name) diff --git a/autoload/clap/picker.vim b/autoload/clap/picker.vim index 0cc74e31d..b9b5e77b2 100644 --- a/autoload/clap/picker.vim +++ b/autoload/clap/picker.vim @@ -48,6 +48,7 @@ function! clap#picker#update(update_info) abort call clap#indicator#update(update_info.matched, update_info.processed) if update_info.matched == 0 + call g:clap.display.clear_highlight() call g:clap.display.set_lines([g:clap_no_matches_msg]) call g:clap.preview.clear() if exists('g:__clap_lines_truncated_map') diff --git a/autoload/clap/plugin/cursorword.vim b/autoload/clap/plugin/cursorword.vim index a537c6454..051148bb9 100644 --- a/autoload/clap/plugin/cursorword.vim +++ b/autoload/clap/plugin/cursorword.vim @@ -12,7 +12,7 @@ hi default link ClapCursorWordTwins ClapUnderline augroup VimClapCursorword autocmd! - autocmd ColorScheme * call clap#client#notify('cursorword.__define-highlights', [+expand('')]) + autocmd ColorScheme * call clap#client#notify('cursorword.__defineHighlights', [+expand('')]) augroup END function! clap#plugin#cursorword#add_highlights(word_highlights) abort diff --git a/autoload/clap/plugin/linter.vim b/autoload/clap/plugin/linter.vim index 2af3d0ad7..d89854724 100644 --- a/autoload/clap/plugin/linter.vim +++ b/autoload/clap/plugin/linter.vim @@ -13,7 +13,8 @@ let s:severity_icons = { \ 'other': '  ', \ } -hi ClapDiagnosticUnderline cterm=underline,bold gui=undercurl,italic,bold ctermfg=173 guifg=#e18254 +hi ClapDiagnosticUnderlineWarn cterm=underline,bold gui=undercurl,italic,bold ctermfg=173 guifg=#e18254 +hi ClapDiagnosticUnderlineError cterm=underline,bold gui=undercurl,italic,bold ctermfg=196 guifg=#f2241f hi DiagnosticWarn ctermfg=136 guifg=#b1951d @@ -153,9 +154,9 @@ function! clap#plugin#linter#display_top_right(current_diagnostics) abort endif endfunction -function! s:highlight_span(bufnr, span) abort +function! s:highlight_span(bufnr, span, hl_group) abort try - call nvim_buf_add_highlight(a:bufnr, s:linter_spans_highlight_ns_id, 'ClapDiagnosticUnderline', a:span.line_start - 1, a:span.column_start - 1, a:span.column_end - 1) + call nvim_buf_add_highlight(a:bufnr, s:linter_spans_highlight_ns_id, a:hl_group, a:span.line_start - 1, a:span.column_start - 1, a:span.column_end - 1) catch " Span may be invalid at this moment since the buffer has been changed. return @@ -222,13 +223,18 @@ endfunction else -function! s:highlight_span(bufnr, span) abort +function! s:highlight_span(bufnr, span, hl_group) abort let length = a:span.column_end - a:span.column_start " It's possible that the span across multiple lines and this can be negative. if length < 0 let length = 1 endif - call prop_add(a:span.line_start, a:span.column_start, { 'type': 'ClapDiagnosticUnderline', 'length': length, 'bufnr': a:bufnr }) + try + call prop_add(a:span.line_start, a:span.column_start, { 'type': a:hl_group, 'length': length, 'bufnr': a:bufnr }) + catch + " Span may be invalid at this moment since the buffer has been changed. + return + endtry endfunction function! clap#plugin#linter#close_top_right() abort @@ -284,17 +290,20 @@ function! clap#plugin#linter#display_top_right(current_diagnostics) abort endif endfunction -call prop_type_add('ClapDiagnosticUnderline', {'highlight': 'ClapDiagnosticUnderline'}) +call prop_type_add('ClapDiagnosticUnderlineWarn', {'highlight': 'ClapDiagnosticUnderlineWarn'}) +call prop_type_add('ClapDiagnosticUnderlineError', {'highlight': 'ClapDiagnosticUnderlineError'}) function! clap#plugin#linter#delete_highlights(bufnr) abort - call prop_remove({ 'type': 'ClapDiagnosticUnderline', 'bufnr': a:bufnr } ) + call prop_remove({ 'type': 'ClapDiagnosticUnderlineWarn', 'bufnr': a:bufnr } ) + call prop_remove({ 'type': 'ClapDiagnosticUnderlineError', 'bufnr': a:bufnr } ) endfunction endif function! clap#plugin#linter#add_highlights(bufnr, diagnostics) abort for diagnostic in a:diagnostics - call map(diagnostic.spans, 's:highlight_span(a:bufnr, v:val)') + let hl_group = diagnostic.severity ==# 'Error' ? 'ClapDiagnosticUnderlineError' : 'ClapDiagnosticUnderlineWarn' + call map(diagnostic.spans, 's:highlight_span(a:bufnr, v:val, hl_group)') endfor endfunction diff --git a/autoload/clap/plugin/lsp.vim b/autoload/clap/plugin/lsp.vim index 34b14392a..1d94c76ef 100644 --- a/autoload/clap/plugin/lsp.vim +++ b/autoload/clap/plugin/lsp.vim @@ -5,8 +5,8 @@ scriptencoding utf-8 let s:save_cpo = &cpoptions set cpoptions&vim -function! s:jump_to(location) abort - execute 'edit' a:location.path +function! clap#plugin#lsp#jump_to(location) abort + execute 'edit!' a:location.path noautocmd call setpos('.', [bufnr(''), a:location.row, a:location.column, 0]) normal! zz endfunction @@ -15,26 +15,9 @@ function! s:to_quickfix_entry(location) abort return { 'filename': a:location.path, 'lnum': a:location.row, 'col': a:location.column, 'text': a:location.text } endfunction -function! clap#plugin#lsp#handle_locations(id, locations) abort - if len(a:locations) == 1 - call s:jump_to(a:locations[0]) - return - endif - - let mode = 'quickfix' - - if mode ==# 'quickfix' - let entries = map(a:locations, 's:to_quickfix_entry(v:val)') - call clap#sink#open_quickfix(entries) - else - let provider = { - \ 'id': a:id, - \ 'source': map(a:locations, 'printf("%s:%s:%s", v:val["path"], v:val["row"], v:val["column"])'), - \ 'sink': 'e', - \ } - - call clap#run(provider) - endif +function! clap#plugin#lsp#populate_quickfix(id, locations) abort + let entries = map(a:locations, 's:to_quickfix_entry(v:val)') + call clap#sink#open_quickfix(entries) endfunction function! clap#plugin#lsp#open_picker(title) abort @@ -66,9 +49,15 @@ function! clap#plugin#lsp#detach(bufnr) abort endfunction if has('nvim') -" [bufnr, changedtick, firstline, lastline, new_lastline] function! clap#plugin#lsp#on_lines(...) abort - call clap#client#notify('lsp.__did_change', a:000) + let [bufnr, changedtick, firstline, lastline, new_lastline] = a:000 + call clap#client#notify('lsp.__didChange', { + \ 'bufnr': bufnr, + \ 'changedtick': changedtick, + \ 'firstline': firstline, + \ 'lastline': lastline, + \ 'new_lastline': new_lastline, + \ }) endfunction function! clap#plugin#lsp#buf_attach(bufnr) abort @@ -93,7 +82,14 @@ endfunction else function! clap#plugin#lsp#listener(bufnr, start, end, added, changes) abort - echom string([a:bufnr, a:start, a:end, a:added, a:changes]) + call clap#client#notify('lsp.__didChange', { + \ 'bufnr': a:bufnr, + \ 'start': a:start, + \ 'end': a:end, + \ 'added': a:added, + \ 'changes': a:changes, + \ 'changedtick': getbufvar(a:bufnr, 'changedtick'), + \ }) endfunction function! clap#plugin#lsp#buf_attach(bufnr) abort diff --git a/autoload/clap/popup/move_manager.vim b/autoload/clap/popup/move_manager.vim index fc5314edb..5ce6f1c8a 100644 --- a/autoload/clap/popup/move_manager.vim +++ b/autoload/clap/popup/move_manager.vim @@ -167,7 +167,7 @@ function! s:move_manager.scroll_up(winid) abort call win_execute(a:winid, 'noautocmd call clap#navigation#scroll("up")') endfunction -" noautocmd is neccessary in that too many plugins use redir, otherwise we'll +" noautocmd is necessary in that too many plugins use redir, otherwise we'll " see E930: Cannot use :redir inside execute(). let s:move_manager["\"] = s:move_manager.ctrl_a let s:move_manager["\"] = s:move_manager.ctrl_b @@ -231,7 +231,7 @@ function! s:move_manager.printable(key) abort " Always hold a delay before reacting actually. " " FIXME - " If the slow renderring of vim job is resolved, this delay could be removed. + " If the slow rendering of vim job is resolved, this delay could be removed. " " apply_input should happen earlier than mock_input " call s:apply_input('') diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index f54313081..a7cf127f1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,12 +11,10 @@ description = "CLI for vim-clap Rust backend" [dependencies] anyhow = { workspace = true } clap = { workspace = true } -futures = { workspace = true } itertools = { workspace = true } num_cpus = { workspace = true } tokio = { workspace = true, features = ["fs", "rt", "process", "macros", "rt-multi-thread", "sync", "time"] } rayon = { workspace = true } -regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } subprocess = { workspace = true } @@ -29,7 +27,6 @@ icon = { workspace = true } matcher = { workspace = true } maple_config = { workspace = true } maple_core = { workspace = true } -pattern = { workspace = true } printer = { workspace = true } types = { workspace = true } utils = { workspace = true } diff --git a/crates/code_tools/Cargo.toml b/crates/code_tools/Cargo.toml index 4028592b3..b4771a77e 100644 --- a/crates/code_tools/Cargo.toml +++ b/crates/code_tools/Cargo.toml @@ -4,10 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] -async-trait = { workspace = true } cargo_metadata = { workspace = true } once_cell = { workspace = true } -parking_lot = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/code_tools/src/language.rs b/crates/code_tools/src/language.rs index 5e094a949..d3b322da9 100644 --- a/crates/code_tools/src/language.rs +++ b/crates/code_tools/src/language.rs @@ -194,19 +194,19 @@ fn config_inner() -> &'static ConfigurationInner { let mut final_languages = language .into_iter() - .map(|c| { - let c = if let Some(mut config) = user_languages.remove(&c.name) { + .map(|mut c| { + if let Some(user_language_config) = user_languages.remove(&c.name) { // Merge the default language config into the value specified by user. - config.merge(c); - config + c.merge(user_language_config); + + (c.name.clone(), c) } else { - c - }; - (c.name.clone(), c) + (c.name.clone(), c) + } }) .collect::>(); - final_languages.extend(user_languages); + final_languages.extend(user_languages.into_iter().map(|(name, c)| (name, c.into()))); ConfigurationInner { filetypes, @@ -258,10 +258,8 @@ pub fn get_language_server_config( if let Some(user_config) = maple_config::config() .plugin .lsp - .language_server - .get(language_server.as_str()) + .language_server_config(language_server.as_str()) { - let user_config: serde_json::Value = serde_json::from_str(&user_config.to_string()).ok()?; language_server_config.update_config(user_config); } diff --git a/crates/filter/Cargo.toml b/crates/filter/Cargo.toml index 01638b7db..1337490f2 100644 --- a/crates/filter/Cargo.toml +++ b/crates/filter/Cargo.toml @@ -7,15 +7,12 @@ edition.workspace = true [dependencies] parking_lot = { workspace = true } rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } subprocess = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } +serde_json = { workspace = true } icon = { workspace = true } matcher = { workspace = true } -pattern = { workspace = true } printer = { workspace = true } types = { workspace = true } -utils = { workspace = true } diff --git a/crates/maple_config/Cargo.toml b/crates/maple_config/Cargo.toml index 9be18d66f..bc15f9644 100644 --- a/crates/maple_config/Cargo.toml +++ b/crates/maple_config/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true dirs = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } toml = { workspace = true } paths = { workspace = true } diff --git a/crates/maple_config/doc_gen/Cargo.toml b/crates/maple_config/doc_gen/Cargo.toml index 4548489b1..ca2e2dd98 100644 --- a/crates/maple_config/doc_gen/Cargo.toml +++ b/crates/maple_config/doc_gen/Cargo.toml @@ -7,10 +7,8 @@ description = "Generate config markdown document from the inline docs" [dependencies] inflections = "1.1.1" maple_config = { path = "../" } -maple_core = { path = "../../maple_core" } toml = "0.5" syn = { version = "1", features = ["full"] } quote = "1" -proc-macro2 = "1.0" itertools = "0.12.0" toml_edit = { version = "0.21.0", features = [ "parse" ] } diff --git a/crates/maple_config/doc_gen/default_config.toml b/crates/maple_config/doc_gen/default_config.toml index 218cfd04f..bd313b7cd 100644 --- a/crates/maple_config/doc_gen/default_config.toml +++ b/crates/maple_config/doc_gen/default_config.toml @@ -54,13 +54,18 @@ enable = false [plugin.lsp] # Whether to enable this plugin. enable = false -# Whether to include the declaration when invoking goto-reference. +# Whether to include the declaration when invoking `ClapAction lsp.reference`. include-declaration = false +# Specify the list of filetypes for which the lsp plugin will be disabled. +# +# If a filetype is included in this list, the Language Server Protocol (LSP) plugin +# will not be activated for files with that particular type. +filetype-blocklist = [] # Specifies custom languages that are not built into vim-clap. # -# If a language is not included in the default languages supported by vim-clap, -# you can specify it here. Note that for languages not listed in the default -# configuration (check out the full list of supported languages in `languages.toml`), +# This config allows to define a new language or override the default value +# of the built-in language config. Note that if you are defining a new language, +# (check out the full list of supported languages by default in `languages.toml`), # you need to provide associated language server configurations as well. # # # Example @@ -68,13 +73,12 @@ include-declaration = false # ```toml # [[plugin.lsp.language]] # name = "erlang" -# filetype = ["erlang"] +# file-types = ["erlang"] # root-markers = ["rebar.config"] # language-servers = ["erlang-ls"] # # [plugin.lsp.language-server.erlang-ls] # command = "erlang_ls" -# args = ["--transport", "stdio"] # ``` language = [] diff --git a/crates/maple_config/src/lib.rs b/crates/maple_config/src/lib.rs index 2e3c8d0f2..2bc258766 100644 --- a/crates/maple_config/src/lib.rs +++ b/crates/maple_config/src/lib.rs @@ -46,17 +46,30 @@ pub fn load_config_on_startup( CONFIG_FILE .set(config_file) - .expect("Failed to initialize Config file"); + .expect("Failed to initialize Config file on startup"); CONFIG .set(loaded_config) - .expect("Failed to initialize Config"); + .expect("Failed to initialize Config on startup"); (config(), maybe_config_err) } +/// [`Config`] is a global singleton, which will be explicitly initialized using +/// the interface [`load_config_on_startup`] with an optional custom config file +/// location when the program is started from CLI, otherwise it will be initialized +/// from the default config file location, which is useful when the config is read +/// from the test code. pub fn config() -> &'static Config { - CONFIG.get_or_init(|| load_config(None).0) + CONFIG.get_or_init(|| { + let (loaded_config, config_file, _) = load_config(None); + + CONFIG_FILE + .set(config_file) + .expect("Failed to initialize Config file"); + + loaded_config + }) } pub fn config_file() -> &'static PathBuf { @@ -246,6 +259,52 @@ pub struct LinterPluginConfig { pub enable: bool, } +/// Defines a new language config or overrides the default config of a language. +#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct UserLanguageConfig { + /// c-sharp, rust, tsx + pub name: String, + + /// List of filetypes corresponding to this language. + /// + /// Overrides the default value of the corresponding language configuration if it exists. + pub file_types: Option>, + + /// List of file extensions corresponding to this language. + /// + /// Overrides the default value of the corresponding language configuration if it exists. + pub file_extensions: Option>, + + /// List of line comment tokens for this language. + /// + /// Overrides the default value of the corresponding language configuration if it exists. + pub line_comments: Option>, + + /// List of markers indicating project roots for this language. Examples include ".git" and "Cargo.toml". + /// + /// Overrides the default value of the corresponding language configuration if it exists. + pub root_markers: Option>, + + /// List of language servers associated with this language. + /// + /// Overrides the default value of the corresponding language configuration if it exists. + pub language_servers: Option>, +} + +impl From for LanguageConfig { + fn from(c: UserLanguageConfig) -> Self { + Self { + name: c.name, + file_types: c.file_types.unwrap_or_default(), + file_extensions: c.file_extensions.unwrap_or_default(), + line_comments: c.line_comments.unwrap_or_default(), + root_markers: c.root_markers.unwrap_or_default(), + language_servers: c.language_servers.unwrap_or_default(), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfig { @@ -256,7 +315,7 @@ pub struct LanguageConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub file_types: Vec, - /// List of `&filetype` corresponding to this language. + /// List of file extensions corresponding to this language. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub file_extensions: Vec, @@ -272,27 +331,34 @@ pub struct LanguageConfig { } impl LanguageConfig { - pub fn merge(&mut self, other: Self) { - let merge_vec = |v: &mut Vec, other: Vec| { - v.extend(other); + pub fn merge(&mut self, user_language_config: UserLanguageConfig) { + let UserLanguageConfig { + name, + file_types: maybe_file_types, + file_extensions: maybe_file_extensions, + line_comments: maybe_line_comments, + root_markers: maybe_root_markers, + language_servers: maybe_language_servers, + } = user_language_config; + + if self.name != name { + return; + } + + let merge_user_config = |v: &mut Vec, maybe_user_config: Option>| { + if let Some(user_config) = maybe_user_config { + *v = user_config; + } + v.sort(); v.dedup(); }; - let Self { - file_types, - file_extensions, - line_comments, - root_markers, - language_servers, - .. - } = other; - - merge_vec(&mut self.file_types, file_types); - merge_vec(&mut self.file_extensions, file_extensions); - merge_vec(&mut self.line_comments, line_comments); - merge_vec(&mut self.root_markers, root_markers); - merge_vec(&mut self.language_servers, language_servers); + merge_user_config(&mut self.file_types, maybe_file_types); + merge_user_config(&mut self.file_extensions, maybe_file_extensions); + merge_user_config(&mut self.line_comments, maybe_line_comments); + merge_user_config(&mut self.root_markers, maybe_root_markers); + merge_user_config(&mut self.language_servers, maybe_language_servers); } } @@ -303,14 +369,20 @@ pub struct LspPluginConfig { /// Whether to enable this plugin. pub enable: bool, - /// Whether to include the declaration when invoking goto-reference. + /// Whether to include the declaration when invoking `ClapAction lsp.reference`. pub include_declaration: bool, + /// Specify the list of filetypes for which the lsp plugin will be disabled. + /// + /// If a filetype is included in this list, the Language Server Protocol (LSP) plugin + /// will not be activated for files with that particular type. + pub filetype_blocklist: Vec, + /// Specifies custom languages that are not built into vim-clap. /// - /// If a language is not included in the default languages supported by vim-clap, - /// you can specify it here. Note that for languages not listed in the default - /// configuration (check out the full list of supported languages in `languages.toml`), + /// This config allows to define a new language or override the default value + /// of the built-in language config. Note that if you are defining a new language, + /// (check out the full list of supported languages by default in `languages.toml`), /// you need to provide associated language server configurations as well. /// /// # Example @@ -324,9 +396,8 @@ pub struct LspPluginConfig { /// /// [plugin.lsp.language-server.erlang-ls] /// command = "erlang_ls" - /// args = ["--transport", "stdio"] /// ``` - pub language: Vec, + pub language: Vec, /// Specify language server configurations. /// @@ -341,6 +412,16 @@ pub struct LspPluginConfig { pub language_server: HashMap, } +impl LspPluginConfig { + /// Returns the custom language server config if any. + pub fn language_server_config(&self, language_server_name: &str) -> Option { + self.language_server + .get(language_server_name) + .cloned() + .and_then(|v| v.try_into().ok()) + } +} + /// Syntax plugin. #[derive(Serialize, Deserialize, Debug, Default, PartialEq)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] @@ -625,4 +706,21 @@ mod tests { let config = Config::default(); toml::to_string_pretty(&config).expect("Deserialize config is okay"); } + + #[test] + fn test_lsp_language_server_config_to_serde_json_value() { + let toml_content = r#" + [language-server.rust-analyzer] + procMacro.enable = false + procMacro.attributes.enable = false + diagnostics.disabled = [ "unresolved-proc-macro" ] +"#; + + let user_config: LspPluginConfig = + toml::from_str(toml_content).expect("Failed to deserialize config"); + + user_config + .language_server_config("rust-analyzer") + .expect("Convert language server config from toml::Value to serde_json::Value"); + } } diff --git a/crates/maple_core/Cargo.toml b/crates/maple_core/Cargo.toml index 8d2b0a0f6..567e0b4b4 100644 --- a/crates/maple_core/Cargo.toml +++ b/crates/maple_core/Cargo.toml @@ -11,7 +11,6 @@ description = "Core of vim-clap Rust backend" [dependencies] async-trait = { workspace = true } base64 = { workspace = true } -bytecount = { workspace = true } chrono = { workspace = true } chrono-humanize = { workspace = true } clap = { workspace = true } diff --git a/crates/maple_core/src/process/subprocess.rs b/crates/maple_core/src/process/subprocess.rs index d186c7372..3886b73ac 100644 --- a/crates/maple_core/src/process/subprocess.rs +++ b/crates/maple_core/src/process/subprocess.rs @@ -3,7 +3,7 @@ use subprocess::Exec; #[inline] pub fn exec(cmd: Exec) -> std::io::Result> { - // We usually have a decent amount of RAM nowdays. + // We usually have a decent amount of RAM nowadays. Ok(std::io::BufReader::with_capacity( 8 * 1024 * 1024, cmd.stream_stdout() diff --git a/crates/maple_core/src/stdio_server/diagnostics_worker.rs b/crates/maple_core/src/stdio_server/diagnostics_worker.rs index befa9a162..7191938bf 100644 --- a/crates/maple_core/src/stdio_server/diagnostics_worker.rs +++ b/crates/maple_core/src/stdio_server/diagnostics_worker.rs @@ -260,10 +260,10 @@ fn convert_lsp_diagnostic_to_diagnostic(lsp_diag: maple_lsp::lsp::Diagnostic) -> } pub enum WorkerMessage { - EchoDiagnostics(usize), - EchoDiagnosticsAtCursor(usize), - NavigateDiagnostics((usize, DiagnosticKind, Direction)), + ShowDiagnostics(usize), + ShowDiagnosticsAtCursor(usize), ShowDiagnosticsAtCursorInFloatWin(usize), + NavigateDiagnostics((usize, DiagnosticKind, Direction)), ResetBufferDiagnostics(usize), LinterDiagnostics((usize, LinterDiagnostics)), LspDiagnostics(maple_lsp::lsp::PublishDiagnosticsParams), @@ -283,7 +283,7 @@ impl BufferDiagnosticsWorker { async fn run(mut self) -> PluginResult<()> { while let Some(worker_msg) = self.worker_msg_receiver.recv().await { match worker_msg { - WorkerMessage::EchoDiagnostics(bufnr) => { + WorkerMessage::ShowDiagnostics(bufnr) => { if let Some(diagnostics) = self.buffer_diagnostics.get(&bufnr) { let diagnostics = diagnostics.inner.read(); self.vim.echo_message(format!("{diagnostics:?}"))?; @@ -292,7 +292,7 @@ impl BufferDiagnosticsWorker { .echo_message(format!("diagnostics not found for buffer {bufnr}"))?; } } - WorkerMessage::EchoDiagnosticsAtCursor(bufnr) => { + WorkerMessage::ShowDiagnosticsAtCursor(bufnr) => { if let Some(diagnostics) = self.buffer_diagnostics.get(&bufnr) { let Ok(lnum) = self.vim.line(".").await else { continue; diff --git a/crates/maple_core/src/stdio_server/input.rs b/crates/maple_core/src/stdio_server/input.rs index 8ba6cd0c8..2b21c21ab 100644 --- a/crates/maple_core/src/stdio_server/input.rs +++ b/crates/maple_core/src/stdio_server/input.rs @@ -105,9 +105,8 @@ impl Event { /// Converts the notification to an [`Event`]. pub fn parse_notification( notification: RpcNotification, - action_parser: impl Fn(RpcNotification) -> Result, + parse_action: impl Fn(RpcNotification) -> Result, ) -> Result { - use AutocmdEventType::*; use KeyEventType::*; match notification.method.as_str() { @@ -126,17 +125,12 @@ impl Event { "shift-up" => Ok(Self::Key((ShiftUp, notification.params))), "shift-down" => Ok(Self::Key((ShiftDown, notification.params))), "backspace" => Ok(Self::Key((Backspace, notification.params))), - "CursorMoved" => Ok(Self::Autocmd((CursorMoved, notification.params))), - "InsertEnter" => Ok(Self::Autocmd((InsertEnter, notification.params))), - "BufEnter" => Ok(Self::Autocmd((BufEnter, notification.params))), - "BufLeave" => Ok(Self::Autocmd((BufLeave, notification.params))), - "BufDelete" => Ok(Self::Autocmd((BufDelete, notification.params))), - "BufWritePost" => Ok(Self::Autocmd((BufWritePost, notification.params))), - "BufWinEnter" => Ok(Self::Autocmd((BufWinEnter, notification.params))), - "BufWinLeave" => Ok(Self::Autocmd((BufWinLeave, notification.params))), - "TextChanged" => Ok(Self::Autocmd((TextChanged, notification.params))), - "TextChangedI" => Ok(Self::Autocmd((TextChangedI, notification.params))), - _ => Ok(Self::Action(action_parser(notification)?)), + autocmd_or_action => match AutocmdEventType::parse(autocmd_or_action) { + Some(autocmd_event_type) => { + Ok(Self::Autocmd((autocmd_event_type, notification.params))) + } + None => Ok(Self::Action(parse_action(notification)?)), + }, } } } diff --git a/crates/maple_core/src/stdio_server/mod.rs b/crates/maple_core/src/stdio_server/mod.rs index 78fece073..da51cc814 100644 --- a/crates/maple_core/src/stdio_server/mod.rs +++ b/crates/maple_core/src/stdio_server/mod.rs @@ -6,6 +6,7 @@ mod provider; mod request_handler; mod service; mod vim; +mod winbar; pub use self::input::InputHistory; use self::input::{ActionEvent, Event, ProviderEvent}; @@ -24,6 +25,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedReceiver; use tokio::time::Instant; +use types::PLUGIN_ACTION_SEPARATOR; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -51,8 +53,9 @@ pub enum Error { async fn initialize_client(vim: Vim, actions: Vec<&str>, config_err: ConfigError) -> VimResult<()> { config_err.notify_error(&vim)?; - let (mut other_actions, mut system_actions): (Vec<_>, Vec<_>) = - actions.into_iter().partition(|action| action.contains('/')); + let (mut other_actions, mut system_actions): (Vec<_>, Vec<_>) = actions + .into_iter() + .partition(|action| action.contains(PLUGIN_ACTION_SEPARATOR)); other_actions.sort(); system_actions.sort(); let mut clap_actions = system_actions; @@ -87,8 +90,8 @@ struct InitializedService { fn initialize_service(vim: Vim) -> InitializedService { use self::diagnostics_worker::start_buffer_diagnostics_worker; use self::plugin::{ - ActionType, ClapPlugin, ColorizerPlugin, CtagsPlugin, CursorwordPlugin, GitPlugin, - LinterPlugin, LspPlugin, MarkdownPlugin, SyntaxPlugin, SystemPlugin, + ActionType, ClapPlugin, ColorizerPlugin, CtagsPlugin, CursorwordPlugin, DiagnosticsPlugin, + GitPlugin, LinterPlugin, LspPlugin, MarkdownPlugin, SyntaxPlugin, SystemPlugin, }; let mut callable_actions = Vec::new(); @@ -116,6 +119,14 @@ fn initialize_service(vim: Vim) -> InitializedService { if plugin_config.lsp.enable || plugin_config.linter.enable { let diagnostics_worker_msg_sender = start_buffer_diagnostics_worker(vim.clone()); + register_plugin( + Box::new(DiagnosticsPlugin::new( + vim.clone(), + diagnostics_worker_msg_sender.clone(), + )), + None, + ); + if plugin_config.lsp.enable { register_plugin( Box::new(LspPlugin::new( @@ -324,7 +335,7 @@ impl Backend { async fn do_process_notification(&self, notification: RpcNotification) -> Result<(), Error> { let maybe_session_id = notification.session_id(); - let action_parser = |notification: RpcNotification| -> Result { + let parse_action = |notification: RpcNotification| -> Result { for (plugin_id, actions) in self.plugin_actions.lock().iter() { if actions.contains(¬ification.method) { return Ok((*plugin_id, notification.into())); @@ -333,7 +344,7 @@ impl Backend { Err(Error::ParseAction(notification)) }; - match Event::parse_notification(notification, action_parser)? { + match Event::parse_notification(notification, parse_action)? { Event::NewProvider(params) => { let session_id = maybe_session_id.ok_or(Error::MissingSessionId)?; let ctx = Context::new(params, self.vim.clone()).await?; diff --git a/crates/maple_core/src/stdio_server/plugin/ctags.rs b/crates/maple_core/src/stdio_server/plugin/ctags.rs index e1cde06a2..76621c94b 100644 --- a/crates/maple_core/src/stdio_server/plugin/ctags.rs +++ b/crates/maple_core/src/stdio_server/plugin/ctags.rs @@ -1,10 +1,9 @@ use crate::stdio_server::input::{AutocmdEvent, AutocmdEventType, PluginAction}; use crate::stdio_server::plugin::{ClapPlugin, PluginError}; use crate::stdio_server::vim::Vim; +use crate::stdio_server::winbar::update_winbar; use crate::tools::ctags::{BufferTag, Scope}; use icon::IconType; -use itertools::Itertools; -use maple_config::FilePathStyle; use serde::Serialize; use std::collections::HashMap; use std::path::Path; @@ -28,21 +27,6 @@ impl<'a> ScopeRef<'a> { } } -fn shrink_text_to_fit(path: String, max_width: usize) -> String { - if path.len() < max_width { - path - } else { - const DOTS: char = '…'; - let to_shrink_len = path.len() - max_width; - let left = path.chars().take(max_width / 2).collect::(); - let right = path - .chars() - .skip(max_width / 2 + 1 + to_shrink_len) - .collect::(); - format!("{left}{DOTS}{right}") - } -} - #[derive(Debug, maple_derive::ClapPlugin)] #[clap_plugin(id = "ctags")] pub struct CtagsPlugin { @@ -69,104 +53,6 @@ impl CtagsPlugin { } } - async fn update_winbar( - &self, - bufnr: usize, - tag: Option<&BufferTag>, - ) -> Result<(), PluginError> { - const SEP: char = ''; - - let winid = self.vim.bare_call::("win_getid").await?; - let winwidth = self.vim.winwidth(winid).await?; - - let path = self.vim.expand(format!("#{bufnr}")).await?; - - let mut winbar_items = Vec::new(); - - let path_style = &maple_config::config().winbar.file_path_style; - - match path_style { - FilePathStyle::OneSegmentPerComponent => { - // TODO: Cache the filepath section. - let mut segments = path.split(std::path::MAIN_SEPARATOR); - - if let Some(seg) = segments.next() { - winbar_items.push(("Normal", seg.to_string())); - } - - winbar_items.extend( - segments.flat_map(|seg| { - [("LineNr", format!(" {SEP} ")), ("Normal", seg.to_string())] - }), - ); - - // Add icon to the filename. - if let Some(last) = winbar_items.pop() { - winbar_items.extend([ - ("Label", format!("{} ", icon::file_icon(&last.1))), - ("Normal", last.1), - ]); - } - } - FilePathStyle::FullPath => { - let max_width = match tag { - Some(tag) => { - if tag.scope.is_some() { - winwidth / 2 - } else { - winwidth * 2 / 3 - } - } - None => winwidth, - }; - let path = if let Some(home) = dirs::Dirs::base().home_dir().to_str() { - path.replacen(home, "~", 1) - } else { - path - }; - winbar_items.push(("LineNr", shrink_text_to_fit(path, max_width))); - } - } - - if let Some(tag) = tag { - if self.vim.call::("winbufnr", [winid]).await? == bufnr { - if let Some(scope) = &tag.scope { - let mut scope_kind_icon = icon::tags_kind_icon(&scope.scope_kind).to_string(); - scope_kind_icon.push(' '); - let scope_max_width = winwidth / 4 - scope_kind_icon.len(); - winbar_items.extend([ - ("LineNr", format!(" {SEP} ")), - ("Include", scope_kind_icon), - ( - "ModeMsg", - shrink_text_to_fit(scope.scope.clone(), scope_max_width), - ), - ]); - } - - let tag_kind_icon = icon::tags_kind_icon(&tag.kind).to_string(); - winbar_items.extend([ - ("LineNr", format!(" {SEP} ")), - ("Type", tag_kind_icon), - ("ModeMsg", format!(" {}", &tag.name)), - ]); - } - } - - if winbar_items.is_empty() { - self.vim.exec("clap#api#update_winbar", (winid, ""))?; - } else { - let winbar = winbar_items - .iter() - .map(|(highlight, value)| format!("%#{highlight}#{value}%*")) - .join(""); - - self.vim.exec("clap#api#update_winbar", (winid, winbar))?; - } - - Ok(()) - } - // Update states if there is no symbol found at current position. async fn on_no_symbol_found( &mut self, @@ -177,7 +63,7 @@ impl CtagsPlugin { let should_reset_winbar = self.last_cursor_tag.take().is_some(); if winbar_enabled && should_reset_winbar { - self.update_winbar(bufnr, None).await?; + update_winbar(&self.vim, bufnr, None).await?; // Redraw the statusline to reflect the latest tag. self.vim.exec("execute", ["redrawstatus"])?; @@ -235,7 +121,7 @@ impl CtagsPlugin { )?; if winbar_enabled { - self.update_winbar(bufnr, Some(tag)).await?; + update_winbar(&self.vim, bufnr, Some(tag)).await?; } // Redraw the statusline to reflect the latest tag. diff --git a/crates/maple_core/src/stdio_server/plugin/cursorword.rs b/crates/maple_core/src/stdio_server/plugin/cursorword.rs index 3bd1452bd..44c73034f 100644 --- a/crates/maple_core/src/stdio_server/plugin/cursorword.rs +++ b/crates/maple_core/src/stdio_server/plugin/cursorword.rs @@ -114,7 +114,7 @@ async fn define_highlights(vim: &Vim) -> Result<(), PluginError> { } #[derive(Debug, maple_derive::ClapPlugin)] -#[clap_plugin(id = "cursorword", actions = ["__define-highlights"])] +#[clap_plugin(id = "cursorword", actions = ["__defineHighlights"])] pub struct Cursorword { vim: Vim, bufs: HashMap, diff --git a/crates/maple_core/src/stdio_server/plugin/diagnostics.rs b/crates/maple_core/src/stdio_server/plugin/diagnostics.rs new file mode 100644 index 000000000..7da6d28cd --- /dev/null +++ b/crates/maple_core/src/stdio_server/plugin/diagnostics.rs @@ -0,0 +1,102 @@ +use crate::stdio_server::diagnostics_worker::WorkerMessage; +use crate::stdio_server::plugin::{ClapPlugin, PluginAction, PluginError}; +use crate::stdio_server::vim::{Vim, VimResult}; +use crate::types::{DiagnosticKind, Direction}; +use tokio::sync::mpsc::UnboundedSender; + +/// This plugin itself does not do any actual work, it is intended to provide the interface +/// for the diagnostics collectively provided by the linter and lsp plugin. +#[derive(Debug, Clone, maple_derive::ClapPlugin)] +#[clap_plugin( + id = "diagnostics", + actions = [ + // Show the diagnostics in the current buffer. + "buffer", + // Show the diagnostics in the cursor line. + "cursor", + "firstError", + "lastError", + "nextError", + "prevError", + "firstWarn", + "lastWarn", + "nextWarn", + "prevWarn", + ] +)] +pub struct Diagnostics { + vim: Vim, + diagnostics_worker_msg_sender: UnboundedSender, +} + +impl Diagnostics { + pub fn new(vim: Vim, diagnostics_worker_msg_sender: UnboundedSender) -> Self { + Self { + vim, + diagnostics_worker_msg_sender, + } + } + + async fn navigate_diagnostics( + &self, + kind: DiagnosticKind, + direction: Direction, + ) -> VimResult<()> { + let bufnr = self.vim.bufnr("").await?; + let _ = self + .diagnostics_worker_msg_sender + .send(WorkerMessage::NavigateDiagnostics((bufnr, kind, direction))); + Ok(()) + } +} + +#[async_trait::async_trait] +impl ClapPlugin for Diagnostics { + async fn handle_action(&mut self, action: PluginAction) -> Result<(), PluginError> { + use DiagnosticKind::{Error, Warn}; + use Direction::{First, Last, Next, Prev}; + + let PluginAction { method, params: _ } = action; + + match self.parse_action(method)? { + DiagnosticsAction::Buffer => { + let bufnr = self.vim.bufnr("").await?; + let _ = self + .diagnostics_worker_msg_sender + .send(WorkerMessage::ShowDiagnostics(bufnr)); + } + DiagnosticsAction::Cursor => { + let bufnr = self.vim.bufnr("").await?; + let _ = self + .diagnostics_worker_msg_sender + .send(WorkerMessage::ShowDiagnosticsAtCursor(bufnr)); + } + DiagnosticsAction::FirstError => { + self.navigate_diagnostics(Error, First).await?; + } + DiagnosticsAction::LastError => { + self.navigate_diagnostics(Error, Last).await?; + } + DiagnosticsAction::NextError => { + self.navigate_diagnostics(Error, Next).await?; + } + DiagnosticsAction::PrevError => { + self.navigate_diagnostics(Error, Prev).await?; + } + DiagnosticsAction::FirstWarn => { + self.navigate_diagnostics(Warn, First).await?; + } + DiagnosticsAction::LastWarn => { + self.navigate_diagnostics(Warn, Last).await?; + } + DiagnosticsAction::NextWarn => { + self.navigate_diagnostics(Warn, Next).await?; + } + DiagnosticsAction::PrevWarn => { + self.navigate_diagnostics(Warn, Prev).await?; + } + } + + Ok(()) + } +} diff --git a/crates/maple_core/src/stdio_server/plugin/git.rs b/crates/maple_core/src/stdio_server/plugin/git.rs index 8419ff503..714d341a5 100644 --- a/crates/maple_core/src/stdio_server/plugin/git.rs +++ b/crates/maple_core/src/stdio_server/plugin/git.rs @@ -25,10 +25,10 @@ struct ModificationState { id = "git", actions = [ "blame", - "diff-summary", - "hunk-modifications", + "diffSummary", + "hunkModifications", "permalink", - "open-permalink-in-browser", + "openPermalinkInBrowser", "toggle", ])] pub struct Git { @@ -205,13 +205,14 @@ impl Git { &self, git: &GitRepo, filepath: &Path, + lnum: usize, ) -> Result, PluginError> { let relative_path = filepath.strip_prefix(&git.repo)?; - let lnum = self.vim.line(".").await?; + let bufnr = self.vim.bufnr(filepath.display().to_string()).await?; - let stdout = if self.vim.bufmodified("").await? { - let lines = self.vim.getbufline("", 1, "$").await?; + let stdout = if self.vim.bufmodified(bufnr).await? { + let lines = self.vim.getbufline(bufnr, 1, "$").await?; git.fetch_blame_output_with_lines(relative_path, lnum, lines)? } else { git.fetch_blame_output(relative_path, lnum)? @@ -233,7 +234,8 @@ impl Git { async fn show_curline_line_blame(&self, bufnr: usize) -> Result<(), PluginError> { if let Some((filepath, git)) = self.bufs.get(&bufnr) { - let maybe_blame_info = self.cursor_line_blame_info(git, filepath).await?; + let lnum = self.vim.line(".").await?; + let maybe_blame_info = self.cursor_line_blame_info(git, filepath, lnum).await?; if let Some(blame_info) = maybe_blame_info { self.vim.exec( "clap#plugin#git#show_cursor_blame_info", @@ -252,8 +254,9 @@ impl Git { return Ok(()); }; + let lnum = self.vim.line(".").await?; if let Ok(Some(blame_info)) = self - .cursor_line_blame_info(&GitRepo::init(git_root.to_path_buf())?, &filepath) + .cursor_line_blame_info(&GitRepo::init(git_root.to_path_buf())?, &filepath, lnum) .await { self.vim.echo_info(blame_info)?; diff --git a/crates/maple_core/src/stdio_server/plugin/linter.rs b/crates/maple_core/src/stdio_server/plugin/linter.rs index ef457c76b..06473bbd9 100644 --- a/crates/maple_core/src/stdio_server/plugin/linter.rs +++ b/crates/maple_core/src/stdio_server/plugin/linter.rs @@ -2,7 +2,6 @@ use crate::stdio_server::diagnostics_worker::WorkerMessage; use crate::stdio_server::input::{AutocmdEvent, AutocmdEventType}; use crate::stdio_server::plugin::{ClapPlugin, PluginAction, PluginError, Toggle}; use crate::stdio_server::vim::{Vim, VimResult}; -use crate::types::{DiagnosticKind, Direction}; use std::collections::HashMap; use std::path::PathBuf; use tokio::sync::mpsc::UnboundedSender; @@ -28,17 +27,7 @@ impl BufferInfo { #[clap_plugin( id = "linter", actions = [ - "echo-diagnostics", - "echo-diagnostics-at-cursor", "format", - "first-error", - "last-error", - "next-error", - "prev-error", - "first-warn", - "last-warn", - "next-warn", - "prev-warn", "debug", "toggle", ] @@ -135,18 +124,6 @@ impl Linter { Ok(()) } - async fn navigate_diagnostics( - &self, - kind: DiagnosticKind, - direction: Direction, - ) -> VimResult<()> { - let bufnr = self.vim.bufnr("").await?; - let _ = self - .diagnostics_worker_msg_sender - .send(WorkerMessage::NavigateDiagnostics((bufnr, kind, direction))); - Ok(()) - } - async fn on_cursor_moved(&self, bufnr: usize) -> VimResult<()> { let _ = self .diagnostics_worker_msg_sender @@ -190,9 +167,6 @@ impl ClapPlugin for Linter { } async fn handle_action(&mut self, action: PluginAction) -> Result<(), PluginError> { - use DiagnosticKind::{Error, Warn}; - use Direction::{First, Last, Next, Prev}; - let PluginAction { method, params: _ } = action; match self.parse_action(method)? { LinterAction::Toggle => { @@ -209,18 +183,6 @@ impl ClapPlugin for Linter { } self.toggle.switch(); } - LinterAction::EchoDiagnostics => { - let bufnr = self.vim.bufnr("").await?; - let _ = self - .diagnostics_worker_msg_sender - .send(WorkerMessage::EchoDiagnostics(bufnr)); - } - LinterAction::EchoDiagnosticsAtCursor => { - let bufnr = self.vim.bufnr("").await?; - let _ = self - .diagnostics_worker_msg_sender - .send(WorkerMessage::EchoDiagnosticsAtCursor(bufnr)); - } LinterAction::Debug => { let bufnr = self.vim.bufnr("").await?; self.on_buf_enter(bufnr).await?; @@ -229,30 +191,6 @@ impl ClapPlugin for Linter { let bufnr = self.vim.bufnr("").await?; self.format_buffer(bufnr).await?; } - LinterAction::FirstError => { - self.navigate_diagnostics(Error, First).await?; - } - LinterAction::LastError => { - self.navigate_diagnostics(Error, Last).await?; - } - LinterAction::NextError => { - self.navigate_diagnostics(Error, Next).await?; - } - LinterAction::PrevError => { - self.navigate_diagnostics(Error, Prev).await?; - } - LinterAction::FirstWarn => { - self.navigate_diagnostics(Warn, First).await?; - } - LinterAction::LastWarn => { - self.navigate_diagnostics(Warn, Last).await?; - } - LinterAction::NextWarn => { - self.navigate_diagnostics(Warn, Next).await?; - } - LinterAction::PrevWarn => { - self.navigate_diagnostics(Warn, Prev).await?; - } } Ok(()) diff --git a/crates/maple_core/src/stdio_server/plugin/lsp.rs b/crates/maple_core/src/stdio_server/plugin/lsp.rs index 48aaf7abe..209d5a7fd 100644 --- a/crates/maple_core/src/stdio_server/plugin/lsp.rs +++ b/crates/maple_core/src/stdio_server/plugin/lsp.rs @@ -5,11 +5,13 @@ use crate::stdio_server::input::{AutocmdEvent, AutocmdEventType}; use crate::stdio_server::plugin::{ClapPlugin, PluginAction, PluginError, Toggle}; use crate::stdio_server::provider::lsp::{set_lsp_source, LspSource}; use crate::stdio_server::vim::{Vim, VimError, VimResult}; +use crate::types::{Goto, GotoLocationsUI}; use code_tools::language::{ find_lsp_root, get_language_server_config, get_root_markers, language_id_from_filetype, language_id_from_path, }; use handler::LanguageServerMessageHandler; +use itertools::Itertools; use lsp::Url; use maple_lsp::lsp; use std::collections::hash_map::Entry; @@ -22,12 +24,12 @@ use tokio::sync::mpsc::UnboundedSender; pub enum Error { #[error("lsp client not found")] ClientNotFound, - #[error("language config not found: {0}")] - UnsupportedLanguage(LanguageId), #[error("buffer not attached")] BufferNotAttached(usize), #[error("invalid Url: {0}")] InvalidUrl(String), + #[error("invalid params: {0}")] + InvalidParams(String), #[error(transparent)] Vim(#[from] VimError), #[error(transparent)] @@ -55,15 +57,6 @@ struct FileLocation { text: String, } -#[derive(Debug)] -enum Goto { - Definition, - Declaration, - TypeDefinition, - Implementation, - Reference, -} - #[derive(Debug, Clone)] struct GotoRequest { bufnr: usize, @@ -137,16 +130,16 @@ fn preprocess_text_edits(text_edits: Vec) -> Vec { actions = [ "__reload", "__detach", - "__did_change", + "__didChange", "format", "rename", - "document-symbols", - "workspace-symbols", - "goto-definition", - "goto-declaration", - "goto-type-definition", - "goto-implementation", - "goto-reference", + "documentSymbols", + "workspaceSymbols", + "definition", + "declaration", + "typeDefinition", + "implementation", + "reference", "toggle", ] )] @@ -156,99 +149,167 @@ pub struct LspPlugin { clients: HashMap>, /// Documents being tracked, keyed by buffer number. attached_buffers: HashMap, + /// Skip the buffer if its filetype is in this list. + filetype_blocklist: Vec, /// Goto request in fly. current_goto_request: Option, diagnostics_worker_msg_sender: UnboundedSender, toggle: Toggle, } +#[allow(unused)] +#[derive(Debug, serde::Deserialize)] +struct Change { + /// the first line number of the change + lnum: usize, + /// the first line below the change + end: usize, + /// number of lines added; negative if lines were deleted + added: i32, + // first column in "lnum" that was affected by + // the change; one if unknown or the whole line + // was affected; this is a byte index, first + // character has a value of one. + col: usize, +} + +#[allow(unused)] +#[derive(Debug, serde::Deserialize)] +enum DidChangeParams { + /// `:h listener_add()` + #[serde(untagged)] + Vim { + bufnr: usize, + start: usize, + end: usize, + added: i32, + changes: Vec, + changedtick: i32, + }, + #[serde(untagged)] + NeoVim { + bufnr: usize, + changedtick: i32, + firstline: usize, + lastline: usize, + new_lastline: usize, + }, +} + impl LspPlugin { pub fn new( vim: Vim, diagnostics_worker_msg_sender: UnboundedSender, ) -> Self { + const FILETYPE_BLOCKLIST: &[&str] = &["clap_input", "coc-explorer"]; + + let mut filetype_blocklist = maple_config::config().plugin.lsp.filetype_blocklist.clone(); + + // Inject the default blocklist. + filetype_blocklist.extend(FILETYPE_BLOCKLIST.iter().map(|s| s.to_string())); + + filetype_blocklist.sort(); + filetype_blocklist.dedup(); + Self { vim, clients: HashMap::new(), attached_buffers: HashMap::new(), current_goto_request: None, diagnostics_worker_msg_sender, + filetype_blocklist, toggle: Toggle::On, } } async fn buffer_attach(&mut self, bufnr: usize) -> Result<(), Error> { + if self.attached_buffers.contains_key(&bufnr) { + tracing::debug!("buffer {bufnr} already attached"); + return Ok(()); + } + + let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; + + if filetype.is_empty() || self.filetype_blocklist.contains(&filetype) { + return Ok(()); + } + let path = self.vim.bufabspath(bufnr).await?; - let language_id = match language_id_from_path(&path) { + let language_id = match language_id_from_filetype(&filetype) { Some(v) => v, - None => { - let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; - - match language_id_from_filetype(&filetype) { - Some(v) => v, - None => { - return Ok(()); - } + None => match language_id_from_path(&path) { + Some(v) => v, + None => { + tracing::debug!( + filetype, + path, + "can not identify the language for buffer {bufnr}" + ); + return Ok(()); } - } + }, }; - let language_server_config = get_language_server_config(language_id) - .ok_or(Error::UnsupportedLanguage(language_id))?; + tracing::debug!(language_id, bufnr, "buffer attached"); - if let Entry::Vacant(e) = self.attached_buffers.entry(bufnr) { - let bufname = self.vim.bufname(bufnr).await?; - let buffer = Buffer { - language_id, - bufname, - doc_id: doc_id(&path)?, - }; + let Some(language_server_config) = get_language_server_config(language_id) else { + tracing::warn!("language server config not found for {language_id}"); + return Ok(()); + }; - match self.clients.entry(language_id) { - Entry::Occupied(e) => { - let root_uri = find_lsp_root(language_id, path.as_ref()) - .and_then(|p| Url::from_file_path(p).ok()); - let client = e.get(); - client.try_add_workspace(root_uri)?; - open_new_doc(client, buffer.language_id, &path)?; - } - Entry::Vacant(e) => { - let enable_snippets = false; - let name = language_server_config.server_name(); - let client = maple_lsp::start_client( - maple_lsp::ClientParams { - language_server_config, - manual_roots: vec![], - enable_snippets, - }, - name.clone(), - Some(PathBuf::from(path.clone())), - get_root_markers(language_id), - LanguageServerMessageHandler::new( - name, - self.vim.clone(), - self.diagnostics_worker_msg_sender.clone(), - ), - ) - .await?; - - open_new_doc(&client, buffer.language_id, &path)?; - - e.insert(client); - } + let bufname = self.vim.bufname(bufnr).await?; + let buffer = Buffer { + language_id, + bufname, + doc_id: doc_id(&path)?, + }; + + match self.clients.entry(language_id) { + Entry::Occupied(e) => { + let root_uri = find_lsp_root(language_id, path.as_ref()) + .and_then(|p| Url::from_file_path(p).ok()); + let client = e.get(); + client.try_add_workspace(root_uri)?; + open_new_doc(client, buffer.language_id, &path)?; + } + Entry::Vacant(e) => { + let enable_snippets = false; + let name = language_server_config.server_name(); + let client = maple_lsp::start_client( + maple_lsp::ClientParams { + language_server_config, + manual_roots: vec![], + enable_snippets, + }, + name.clone(), + Some(PathBuf::from(path.clone())), + get_root_markers(language_id), + LanguageServerMessageHandler::new( + name, + self.vim.clone(), + self.diagnostics_worker_msg_sender.clone(), + ), + ) + .await?; + + open_new_doc(&client, buffer.language_id, &path)?; + + e.insert(client); } + } - self.vim.exec("clap#plugin#lsp#buf_attach", [bufnr])?; + self.vim.exec("clap#plugin#lsp#buf_attach", [bufnr])?; - e.insert(buffer); - } + self.attached_buffers.insert(bufnr, buffer); Ok(()) } - fn buffer_detach(&mut self, bufnr: usize) -> Result<(), Error> { + fn buffer_detach(&mut self, [bufnr]: [usize; 1]) -> Result<(), Error> { if let Some(buffer) = self.attached_buffers.remove(&bufnr) { + tracing::debug!(bufnr, "buffer detached"); + let client = self .clients .get(&buffer.language_id) @@ -261,7 +322,7 @@ impl LspPlugin { Ok(()) } - async fn reload_document(&mut self, bufnr: usize) -> Result<(), Error> { + async fn reload_document(&mut self, [bufnr]: [usize; 1]) -> Result<(), Error> { let buffer = self .attached_buffers .get_mut(&bufnr) @@ -290,14 +351,26 @@ impl LspPlugin { async fn text_document_did_change( &self, - (bufnr, changedtick, _firstline, _lastline, _new_lastline): ( - usize, - i32, - usize, - usize, - usize, - ), + did_change_params: DidChangeParams, ) -> Result<(), Error> { + let (bufnr, changedtick) = match did_change_params { + DidChangeParams::Vim { + bufnr, + start: _, + end: _, + added: _, + changes: _, + changedtick, + } => (bufnr, changedtick), + DidChangeParams::NeoVim { + bufnr, + changedtick, + firstline: _, + lastline: _, + new_lastline: _, + } => (bufnr, changedtick), + }; + let document = self.get_buffer(bufnr)?; let client = self @@ -320,10 +393,10 @@ impl LspPlugin { } async fn text_document_did_save(&mut self, bufnr: usize) -> Result<(), Error> { - let buffer = self - .attached_buffers - .get_mut(&bufnr) - .ok_or(Error::BufferNotAttached(bufnr))?; + let Some(buffer) = self.attached_buffers.get_mut(&bufnr) else { + // Buffer not attached. + return Ok(()); + }; let client = self .clients @@ -408,8 +481,28 @@ impl LspPlugin { Ok(Some(cursor_lsp_position)) } - async fn goto_impl(&mut self, goto: Goto) -> Result<(), Error> { - let bufnr = self.vim.bufnr("").await?; + async fn goto_impl(&mut self, goto: Goto, params: rpc::Params) -> Result<(), Error> { + let (bufnr, row, column) = match params { + rpc::Params::Array(array) => { + if array.is_empty() { + self.vim.get_cursor_pos().await? + } else { + let (bufnr, row, column): (u64, u64, u64) = array + .iter() + .filter_map(|v| v.as_u64()) + .collect_tuple() + .ok_or_else(|| { + Error::InvalidParams(format!( + "expect [usize, usize, usize], got: {array:?}" + )) + })?; + + (bufnr as usize, row as usize, column as usize) + } + } + _ => self.vim.get_cursor_pos().await?, + }; + let document = self.get_buffer(bufnr)?; let client = self @@ -418,7 +511,6 @@ impl LspPlugin { .ok_or(Error::ClientNotFound)?; let path = self.vim.bufabspath(bufnr).await?; - let (bufnr, row, column) = self.vim.get_cursor_pos().await?; let position = lsp::Position { line: row as u32 - 1, character: column as u32 - 1, @@ -478,33 +570,48 @@ impl LspPlugin { return Ok(()); } - let locations = locations - .into_iter() - .map(|loc| { - let path = loc.uri.path(); - let row = loc.range.start.line + 1; - let column = loc.range.start.character + 1; - let text = utils::read_line_at(path, row as usize) - .ok() - .flatten() - .unwrap_or_default(); - - FileLocation { - path: path.to_string(), - row, - column, - text, - } - }) - .collect::>(); - - self.vim.exec( - "clap#plugin#lsp#handle_locations", - (format!("{goto:?}"), locations), - )?; self.vim.update_lsp_status("rust-analyzer")?; self.current_goto_request.take(); + if locations.len() == 1 { + self.vim.exec("clap#plugin#lsp#jump_to", [&locations[0]])?; + return Ok(()); + } + + let mode = GotoLocationsUI::ClapProvider; + + match mode { + GotoLocationsUI::Quickfix => { + let locations = locations + .into_iter() + .map(|loc| { + let path = loc.uri.path(); + let row = loc.range.start.line + 1; + let column = loc.range.start.character + 1; + let text = utils::read_line_at(path, row as usize) + .ok() + .flatten() + .unwrap_or_default(); + + FileLocation { + path: path.to_string(), + row, + column, + text, + } + }) + .collect::>(); + + self.vim.exec( + "clap#plugin#lsp#populate_quickfix", + (format!("{goto:?}"), locations), + )?; + } + GotoLocationsUI::ClapProvider => { + self.open_picker(LspSource::Locations((goto, locations)))?; + } + } + Ok(()) } @@ -512,6 +619,13 @@ impl LspPlugin { let title = match lsp_source { LspSource::DocumentSymbols(_) => "documentSymbols", LspSource::WorkspaceSymbols(_) => "workspaceSymbols", + LspSource::Locations((goto, _)) => match goto { + Goto::Reference => "references", + Goto::Declaration => "declarations", + Goto::Definition => "definitions", + Goto::TypeDefinition => "typeDefinitions", + Goto::Implementation => "implementations", + }, LspSource::Empty => unreachable!("source must not be empty to open"), }; set_lsp_source(lsp_source); @@ -785,7 +899,7 @@ impl ClapPlugin for LspPlugin { self.text_document_did_save(bufnr).await?; } BufDelete => { - self.buffer_detach(bufnr)?; + self.buffer_detach([bufnr])?; } CursorMoved => { self.on_cursor_moved(bufnr).await?; @@ -812,26 +926,26 @@ impl ClapPlugin for LspPlugin { } self.toggle.switch(); } - LspAction::Format => { - self.text_document_format().await?; + LspAction::Definition => { + self.goto_impl(Goto::Definition, params).await?; } - LspAction::Rename => { - self.rename_symbol().await?; + LspAction::Declaration => { + self.goto_impl(Goto::Declaration, params).await?; } - LspAction::GotoDefinition => { - self.goto_impl(Goto::Definition).await?; + LspAction::TypeDefinition => { + self.goto_impl(Goto::TypeDefinition, params).await?; } - LspAction::GotoDeclaration => { - self.goto_impl(Goto::Declaration).await?; + LspAction::Implementation => { + self.goto_impl(Goto::Implementation, params).await?; } - LspAction::GotoTypeDefinition => { - self.goto_impl(Goto::TypeDefinition).await?; + LspAction::Reference => { + self.goto_impl(Goto::Reference, params).await?; } - LspAction::GotoImplementation => { - self.goto_impl(Goto::Implementation).await?; + LspAction::Format => { + self.text_document_format().await?; } - LspAction::GotoReference => { - self.goto_impl(Goto::Reference).await?; + LspAction::Rename => { + self.rename_symbol().await?; } LspAction::DocumentSymbols => { self.document_symbols().await?; diff --git a/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs b/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs index 5c6612abe..1c2981f9e 100644 --- a/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs +++ b/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs @@ -162,8 +162,13 @@ impl HandleLanguageServerMessage for LanguageServerMessageHandler { } } } + LanguageServerNotification::ShowMessage(params) => { + let _ = self + .vim + .echo_message(format!("[{}] {}", self.server_name, params.message)); + } _ => { - tracing::debug!("TODO: handle language server notification"); + tracing::debug!("TODO: handle language server notification: {notification:?}"); } } diff --git a/crates/maple_core/src/stdio_server/plugin/markdown.rs b/crates/maple_core/src/stdio_server/plugin/markdown.rs index 85407b9bd..ceed565c9 100644 --- a/crates/maple_core/src/stdio_server/plugin/markdown.rs +++ b/crates/maple_core/src/stdio_server/plugin/markdown.rs @@ -199,7 +199,7 @@ fn find_toc_range(input_file: impl AsRef) -> std::io::Result Result<(), PluginError> { let fpath = self.vim.bufabspath(bufnr).await?; - let maybe_extension = std::path::Path::new(&fpath) - .extension() - .and_then(|e| e.to_str()); + let maybe_extension = Path::new(&fpath).extension().and_then(|e| e.to_str()); if let Some(extension) = maybe_extension { self.sublime_bufs.insert(bufnr, extension.to_string()); @@ -172,8 +170,7 @@ impl Syntax { buf_modified: bool, maybe_language: Option, ) -> Result<(), PluginError> { - let source_file = self.vim.bufabspath(bufnr).await?; - let source_file = std::path::PathBuf::from(source_file); + let source_file = PathBuf::from(self.vim.bufabspath(bufnr).await?); let language = match maybe_language { Some(language) => language, @@ -210,7 +207,7 @@ impl Syntax { let file_size = FileSize(source_code.len()); - tracing::debug!( + tracing::trace!( ?language, highlighted_lines = raw_highlights.len(), %file_size, @@ -290,7 +287,7 @@ impl Syntax { return Ok(None); } - tracing::debug!( + tracing::trace!( total = new_vim_highlights.len(), unchanged_lines_count = unchanged_lines.len(), changed_lines_count = changed_lines.len(), @@ -334,8 +331,7 @@ impl Syntax { bufnr: usize, language: Language, ) -> Result<(), PluginError> { - let source_file = self.vim.bufabspath(bufnr).await?; - let source_file = std::path::PathBuf::from(source_file); + let source_file = PathBuf::from(self.vim.bufabspath(bufnr).await?); let source_code = std::fs::read(&source_file)?; @@ -371,6 +367,9 @@ impl Syntax { } else { self.vim.echo_message("tree sitter props not found")?; } + } else { + self.vim + .echo_message("tree sitter highlight not enabled for this buffer")?; } Ok(()) @@ -391,6 +390,7 @@ impl From> for HighlightRange { } impl HighlightRange { + /// Returns `true` if the line at specified line number should be highlighted. fn should_highlight(&self, line_number: usize) -> bool { match self { Self::Lines(range) => range.contains(&line_number), diff --git a/crates/maple_core/src/stdio_server/plugin/system.rs b/crates/maple_core/src/stdio_server/plugin/system.rs index c13c9231c..e73d36d5f 100644 --- a/crates/maple_core/src/stdio_server/plugin/system.rs +++ b/crates/maple_core/src/stdio_server/plugin/system.rs @@ -11,12 +11,12 @@ use std::collections::HashMap; #[clap_plugin( id = "system", actions = [ - "__note_recent_files", - "__copy-to-clipboard", - "__configure-vim-which-key", - "__did-you-mean", - "open-config", - "list-plugins", + "__noteRecentFiles", + "__copyToClipboard", + "__configureVimWhichKey", + "__didYouMean", + "openConfig", + "listPlugins", ] )] pub struct System { diff --git a/crates/maple_core/src/stdio_server/provider/impls/grep.rs b/crates/maple_core/src/stdio_server/provider/impls/grep.rs index 70c3e4bb5..275fb8212 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/grep.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/grep.rs @@ -28,9 +28,7 @@ pub struct GrepProvider { impl GrepProvider { pub async fn new(ctx: &Context) -> Result { - tracing::debug!("================== [grep] ctx: {ctx:?}"); let GrepArgs { base, paths } = ctx.parse_provider_args().await?; - tracing::debug!("================== [grep] base: {base:?}, paths: {paths:?}"); Ok(Self { args: GrepArgs { base, diff --git a/crates/maple_core/src/stdio_server/provider/impls/lsp.rs b/crates/maple_core/src/stdio_server/provider/impls/lsp.rs index 9a7d54b78..15b5e37e2 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/lsp.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/lsp.rs @@ -1,5 +1,6 @@ use crate::stdio_server::provider::hooks::PreviewTarget; use crate::stdio_server::provider::{ClapProvider, Context, ProviderResult as Result}; +use crate::types::Goto; use maple_lsp::lsp; use matcher::MatchScope; use once_cell::sync::Lazy; @@ -14,6 +15,7 @@ use types::{ClapItem, FuzzyText, Query}; pub enum LspSource { DocumentSymbols((lsp::Url, Vec)), WorkspaceSymbols(Vec), + Locations((Goto, Vec)), Empty, } @@ -87,6 +89,53 @@ fn to_kind_str(kind: lsp::SymbolKind) -> &'static str { } } +#[derive(Debug)] +pub struct LocationItem { + pub file_path: String, + pub output_text: String, +} + +impl LocationItem { + fn from_location(location: &lsp::Location, project_root: &str) -> Option { + let file_path = location + .uri + .to_file_path() + .ok()? + .to_string_lossy() + .to_string(); + let start_line = location.range.start.line; + let start_character = location.range.start.character; + let line = utils::read_line_at(&file_path, start_line as usize) + .ok() + .flatten()?; + + let path = file_path + .strip_prefix(project_root) + .unwrap_or(file_path.as_str()); + let path = path.strip_prefix(std::path::MAIN_SEPARATOR).unwrap_or(path); + let output_text = format!("{path}:{start_line}:{start_character}:{line}"); + + Some(Self { + file_path, + output_text, + }) + } +} + +impl ClapItem for LocationItem { + fn raw_text(&self) -> &str { + &self.output_text + } + + fn output_text(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.output_text) + } + + fn icon(&self, _icon: icon::Icon) -> Option { + Some(icon::file_icon(&self.file_path)) + } +} + #[derive(Debug)] pub struct DocumentItem { pub name: String, @@ -190,6 +239,7 @@ impl ClapItem for WorkspaceItem { enum SourceItems { Document((lsp::Url, Vec>)), Workspace(Vec>), + Locations((Goto, Vec>)), Empty, } @@ -227,6 +277,18 @@ impl LspProvider { SourceItems::Workspace(items) } + LspSource::Locations((goto, ref locations)) => { + let root = ctx.cwd.as_str(); + let items = locations + .iter() + .filter_map(|location| { + LocationItem::from_location(location, root) + .map(|item| Arc::new(item) as Arc) + }) + .collect::>(); + + SourceItems::Locations((goto, items)) + } LspSource::Empty => SourceItems::Empty, }; @@ -242,6 +304,7 @@ impl LspProvider { let items = match &self.source_items { SourceItems::Document((_uri, ref items)) => items, SourceItems::Workspace(ref items) => items, + SourceItems::Locations((_goto, ref items)) => items, SourceItems::Empty => { return Ok(()); } @@ -319,6 +382,19 @@ impl ClapProvider for LspProvider { }) }) } + SourceItems::Locations(_) => { + let curline = ctx.vim.display_getcurline().await?; + let Some((fpath, lnum, _col, _cache_line)) = + pattern::extract_grep_position(&curline) + else { + return Ok(()); + }; + + Some(PreviewTarget::LineInFile { + path: fpath.into(), + line_number: lnum + 1, + }) + } SourceItems::Empty => return Ok(()), }; ctx.update_preview(preview_target).await diff --git a/crates/maple_core/src/stdio_server/winbar.rs b/crates/maple_core/src/stdio_server/winbar.rs new file mode 100644 index 000000000..05757bff0 --- /dev/null +++ b/crates/maple_core/src/stdio_server/winbar.rs @@ -0,0 +1,117 @@ +use crate::stdio_server::plugin::PluginError; +use crate::stdio_server::vim::Vim; +use crate::tools::ctags::BufferTag; +use itertools::Itertools; +use maple_config::FilePathStyle; + +fn shrink_text_to_fit(path: String, max_width: usize) -> String { + if path.len() < max_width { + path + } else { + const DOTS: char = '…'; + let to_shrink_len = path.len() - max_width; + let left = path.chars().take(max_width / 2).collect::(); + let right = path + .chars() + .skip(max_width / 2 + 1 + to_shrink_len) + .collect::(); + format!("{left}{DOTS}{right}") + } +} + +pub async fn update_winbar( + vim: &Vim, + bufnr: usize, + tag: Option<&BufferTag>, +) -> Result<(), PluginError> { + const SEP: char = ''; + + let winid = vim.bare_call::("win_getid").await?; + let winwidth = vim.winwidth(winid).await?; + + let path = vim.expand(format!("#{bufnr}")).await?; + + let mut winbar_items = Vec::new(); + + let path_style = &maple_config::config().winbar.file_path_style; + + match path_style { + FilePathStyle::OneSegmentPerComponent => { + // TODO: Cache the filepath section. + let mut segments = path.split(std::path::MAIN_SEPARATOR); + + if let Some(seg) = segments.next() { + winbar_items.push(("Normal", seg.to_string())); + } + + winbar_items.extend( + segments + .flat_map(|seg| [("LineNr", format!(" {SEP} ")), ("Normal", seg.to_string())]), + ); + + // Add icon to the filename. + if let Some(last) = winbar_items.pop() { + winbar_items.extend([ + ("Label", format!("{} ", icon::file_icon(&last.1))), + ("Normal", last.1), + ]); + } + } + FilePathStyle::FullPath => { + let max_width = match tag { + Some(tag) => { + if tag.scope.is_some() { + winwidth / 2 + } else { + winwidth * 2 / 3 + } + } + None => winwidth, + }; + let path = if let Some(home) = dirs::Dirs::base().home_dir().to_str() { + path.replacen(home, "~", 1) + } else { + path + }; + winbar_items.push(("LineNr", shrink_text_to_fit(path, max_width))); + } + } + + if let Some(tag) = tag { + if vim.call::("winbufnr", [winid]).await? == bufnr { + if let Some(scope) = &tag.scope { + let mut scope_kind_icon = icon::tags_kind_icon(&scope.scope_kind).to_string(); + scope_kind_icon.push(' '); + let scope_max_width = winwidth / 4 - scope_kind_icon.len(); + winbar_items.extend([ + ("LineNr", format!(" {SEP} ")), + ("Include", scope_kind_icon), + ( + "LineNr", + shrink_text_to_fit(scope.scope.clone(), scope_max_width), + ), + ]); + } + + let tag_kind_icon = icon::tags_kind_icon(&tag.kind).to_string(); + winbar_items.extend([ + ("LineNr", format!(" {SEP} ")), + ("Type", tag_kind_icon), + ("LineNr", format!(" {}", &tag.name)), + ]); + } + } + + if winbar_items.is_empty() { + vim.exec("clap#api#update_winbar", (winid, ""))?; + } else { + let winbar = winbar_items + .iter() + .map(|(highlight, value)| format!("%#{highlight}#{value}%*")) + .join(""); + + vim.exec("clap#api#update_winbar", (winid, winbar))?; + } + + Ok(()) +} diff --git a/crates/maple_core/src/types.rs b/crates/maple_core/src/types.rs index 426bc40c7..c6d9fd0f4 100644 --- a/crates/maple_core/src/types.rs +++ b/crates/maple_core/src/types.rs @@ -9,3 +9,18 @@ pub enum DiagnosticKind { Error, Warn, } + +#[derive(Clone, Copy, Debug)] +pub enum Goto { + Definition, + Declaration, + TypeDefinition, + Implementation, + Reference, +} + +#[allow(dead_code)] +pub enum GotoLocationsUI { + Quickfix, + ClapProvider, +} diff --git a/crates/maple_derive/src/impls/clap_plugin.rs b/crates/maple_derive/src/impls/clap_plugin.rs index a4ab618a3..050602f0a 100644 --- a/crates/maple_derive/src/impls/clap_plugin.rs +++ b/crates/maple_derive/src/impls/clap_plugin.rs @@ -2,12 +2,13 @@ use std::collections::HashSet; use std::sync::Mutex; use darling::FromMeta; -use inflections::case::to_pascal_case; +use inflections::case::{is_camel_case, to_kebab_case, to_pascal_case}; use once_cell::sync::Lazy; use proc_macro::{self, TokenStream}; use proc_macro2::Span; use quote::quote; use syn::{DeriveInput, Error, Expr, Ident, LitStr}; +use types::PLUGIN_ACTION_SEPARATOR; static PLUGINS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); @@ -17,9 +18,6 @@ struct Plugin { actions: Option, } -/// Separator for non-system plugin actions, `git.reload`. -const SEP: char = '.'; - pub fn clap_plugin_derive_impl(input: &DeriveInput) -> TokenStream { let mut maybe_plugin_id = None; let mut actions_parsed = Vec::::new(); @@ -83,7 +81,7 @@ pub fn clap_plugin_derive_impl(input: &DeriveInput) -> TokenStream { let mut used_actions = HashSet::new(); - // Generate constants from the attribute values + // Parse actions let constants = actions_parsed.iter().map(|action| { let action_name = action.as_str(); @@ -106,17 +104,17 @@ pub fn clap_plugin_derive_impl(input: &DeriveInput) -> TokenStream { }; let check_operation_validity = |operation: &str| { - let is_valid = operation - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'); + if !operation.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return Some(Error::new( + Span::call_site(), + format!("Invalid character in {action_name}: expect only ASCII alphanumeric character or [-]"), + )); + } - if is_valid { + if is_camel_case(operation) { None } else { - Some(Error::new( - Span::call_site(), - format!("Invalid character in {action_name}: expect only ASCII alphanumeric character or [-_]"), - )) + Some(Error::new(Span::call_site(), format!("{action_name} is not in camelCase"))) } }; @@ -127,19 +125,21 @@ pub fn clap_plugin_derive_impl(input: &DeriveInput) -> TokenStream { raw_actions.push(action_name); - let action_name = action_name.replace('-', "_"); + // __internalAction => __internal-action => __internal_action + let action_name = to_kebab_case(action_name).replace('-', "_"); + // __INTERNAL_ACTION let uppercase_action = action_name.to_uppercase(); let action_lit = Ident::new(&uppercase_action, ident.span()); let action_var = Ident::new(&format!("ACTION_{uppercase_action}"), ident.span()); actions_list.push(action_var.clone()); - // No prefix for system plugin. + // No plugin_id prefix for system plugin. let namespaced_action = if plugin_id == "system" { action.clone() } else { - format!("{plugin_id}{SEP}{action}") + format!("{plugin_id}{PLUGIN_ACTION_SEPARATOR}{action}") }; if is_callable { @@ -168,11 +168,11 @@ pub fn clap_plugin_derive_impl(input: &DeriveInput) -> TokenStream { let action_variants = raw_actions .iter() .map(|arg| { - // "__note-recent-files", "cursorword.__define-highlights" + // "__noteRecentFiles", "cursorword.__defineHighlights" let method = if plugin_id == "system" { arg.to_string() } else { - format!("{plugin_id}{SEP}{arg}") + format!("{plugin_id}{PLUGIN_ACTION_SEPARATOR}{arg}") }; let pascal_name = if let Some(name) = arg.strip_prefix("__") { format!("__{}", to_pascal_case(name)) diff --git a/crates/sublime_syntax/Cargo.toml b/crates/sublime_syntax/Cargo.toml index 8050e56e1..0c7ffb6b6 100644 --- a/crates/sublime_syntax/Cargo.toml +++ b/crates/sublime_syntax/Cargo.toml @@ -5,10 +5,8 @@ edition.workspace = true [dependencies] colors-transform = { workspace = true } -once_cell = { workspace = true } rgb2ansi256 = { workspace = true } serde = { workspace = true, features = ["derive"] } syntect = { workspace = true } -tracing = { workspace = true } utils = { workspace = true } diff --git a/crates/tree_sitter/Cargo.toml b/crates/tree_sitter/Cargo.toml index 05c7e4982..83a7fd9ac 100644 --- a/crates/tree_sitter/Cargo.toml +++ b/crates/tree_sitter/Cargo.toml @@ -12,6 +12,7 @@ tree-sitter-highlight = "0.20" once_cell = { workspace = true } serde = { workspace = true, features = [ "derive" ] } toml = { workspace = true } +tracing = { workspace = true } # tree-sitter-traversal = "0.1" # Languages diff --git a/crates/tree_sitter/src/language.rs b/crates/tree_sitter/src/language.rs index ea67783c8..9d4f38b53 100644 --- a/crates/tree_sitter/src/language.rs +++ b/crates/tree_sitter/src/language.rs @@ -33,33 +33,30 @@ struct ConfigInner { static CONFIG: Lazy = Lazy::new(|| { let tree_sitter_config = include_bytes!("../tree_sitter_config.toml"); let config: Config = toml::from_slice(tree_sitter_config).unwrap(); - config.into_config_inner() -}); - -impl Config { - fn into_config_inner(self) -> ConfigInner { - ConfigInner { - language: self - .language - .into_iter() - .map(|(lang, highlight_config)| { - let lang: Language = lang.parse().unwrap(); - let (names, groups): (Vec<_>, Vec<_>) = highlight_config - .highlight_name_and_groups - .into_iter() - .unzip(); - ( - lang, - HighlightConfigInner { - highlight_names: names, - highlight_groups: groups, - }, - ) - }) - .collect(), - } + ConfigInner { + language: config + .language + .into_iter() + .filter_map(|(lang, highlight_config)| { + let Ok(lang) = lang.parse::() else { + tracing::error!("Invalid language name in tree_sitter_config: {lang}"); + return None; + }; + let (names, groups): (Vec<_>, Vec<_>) = highlight_config + .highlight_name_and_groups + .into_iter() + .unzip(); + Some(( + lang, + HighlightConfigInner { + highlight_names: names, + highlight_groups: groups, + }, + )) + }) + .collect(), } -} +}); /// Small macro to generate a module, declaring the list of highlight name /// in tree_sitter_highlight and associated vim highlight group name. diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 9a646ba13..5e938e2f5 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -71,6 +71,9 @@ pub trait SearchProgressUpdate { ); } +/// Separator for non-system plugin actions, `git.reload`. +pub const PLUGIN_ACTION_SEPARATOR: char = '.'; + /// Plugin interfaces to users. pub trait ClapAction { fn id(&self) -> &'static str; @@ -134,6 +137,13 @@ macro_rules! event_enum_with_variants { pub fn variants() -> &'static [&'static str] { &[ $( stringify!($variant), )* ] } + + pub fn parse(autocmd: &str) -> Option { + match autocmd { + $( stringify!($variant) => Some(Self::$variant), )* + _ => None + } + } } }; } diff --git a/crates/upgrade/Cargo.toml b/crates/upgrade/Cargo.toml index 952fed90d..679a81714 100644 --- a/crates/upgrade/Cargo.toml +++ b/crates/upgrade/Cargo.toml @@ -10,4 +10,3 @@ tokio = { workspace = true, features = ["fs", "macros", "rt", "io-util", "rt-mul # Use `rustls-tls` instead of `default-tls` to not pull in the openssl dep, making the cross-compile easier. reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } serde = { workspace = true } -serde_json = { workspace = true } diff --git a/crates/upgrade/src/github.rs b/crates/upgrade/src/github.rs index 20734039e..a8d3ae4b5 100644 --- a/crates/upgrade/src/github.rs +++ b/crates/upgrade/src/github.rs @@ -1,43 +1,14 @@ +use indicatif::{ProgressBar, ProgressStyle}; use serde::de::DeserializeOwned; use serde::Deserialize; - -const USER: &str = "liuchengxu"; -const REPO: &str = "vim-clap"; - -pub(super) fn asset_name() -> Option<&'static str> { - if cfg!(target_os = "macos") { - if cfg!(target_arch = "x86_64") { - Some("maple-x86_64-apple-darwin") - } else if cfg!(target_arch = "aarch64") { - Some("maple-aarch64-apple-darwin") - } else { - None - } - } else if cfg!(target_os = "linux") { - if cfg!(target_arch = "x86_64") { - Some("maple-x86_64-unknown-linux-musl") - } else if cfg!(target_arch = "aarch64") { - Some("maple-aarch64-unknown-linux-gnu") - } else { - None - } - } else if cfg!(target_os = "windows") { - Some("maple-x86_64-pc-windows-msvc") - } else { - None - } -} - -pub(super) fn asset_download_url(version: &str) -> Option { - asset_name().map(|asset_name| { - format!("https://github.com/{USER}/{REPO}/releases/download/{version}/{asset_name}",) - }) -} +use std::path::PathBuf; +use tokio::io::AsyncWriteExt; #[derive(Debug, Deserialize)] pub struct Asset { pub name: String, pub size: u64, + pub browser_download_url: String, } // https://docs.github.com/en/rest/releases/releases @@ -47,14 +18,14 @@ pub struct GitHubRelease { pub assets: Vec, } -async fn request(url: &str) -> std::io::Result { +pub async fn request(url: &str, user_agent: &str) -> std::io::Result { let io_error = |e| std::io::Error::new(std::io::ErrorKind::Other, format!("Reqwest error: {e}")); reqwest::Client::new() .get(url) .header("Accept", "application/vnd.github.v3+json") - .header("User-Agent", USER) + .header("User-Agent", user_agent) .send() .await .map_err(io_error)? @@ -63,43 +34,71 @@ async fn request(url: &str) -> std::io::Result { .map_err(io_error) } -pub(super) async fn retrieve_asset_size(asset_name: &str, tag: &str) -> std::io::Result { - let url = format!("https://api.github.com/repos/{USER}/{REPO}/releases/tags/{tag}"); - let release: GitHubRelease = request(&url).await?; - - release - .assets - .iter() - .find(|x| x.name == asset_name) - .map(|x| x.size) - .ok_or_else(|| panic!("Can not find the asset {asset_name} in given release {tag}")) +pub async fn latest_github_release(user: &str, repo: &str) -> std::io::Result { + let url = format!("https://api.github.com/repos/{user}/{repo}/releases/latest"); + request::(&url, user).await } -pub(super) async fn retrieve_latest_release() -> std::io::Result { - let url = format!("https://api.github.com/repos/{USER}/{REPO}/releases/latest"); - request::(&url).await +pub enum DownloadResult { + /// File already exists in the specified path. + Existed(PathBuf), + /// File was downloaded successfully to the given path. + Success(PathBuf), } -#[cfg(test)] -mod tests { - use super::*; +/// Download an asset file from GitHub to the local file system. +pub async fn download_asset_file( + version: &str, + asset_name: &str, + total_size: u64, + asset_download_url: &str, + no_progress_bar: bool, +) -> std::io::Result { + let mut tmp = std::env::temp_dir(); + tmp.push(format!("{version}-{asset_name}")); - #[tokio::test] - async fn test_retrieve_asset_size() { - if crate::tests::is_commit_associated_with_a_tag() { - return; + // Check if there is a partially downloaded binary before. + if tmp.is_file() { + let metadata = std::fs::metadata(&tmp)?; + if metadata.len() == total_size { + return Ok(DownloadResult::Existed(tmp)); + } else { + std::fs::remove_file(&tmp)?; } + } - for _i in 0..20 { - if let Ok(latest_tag) = retrieve_latest_release().await.map(|r| r.tag_name) { - retrieve_asset_size(asset_name().unwrap(), &latest_tag) - .await - .expect("Failed to retrieve the asset size for latest release"); - return; - } - tokio::time::sleep(std::time::Duration::from_millis(500)).await; - } + let mut maybe_progress_bar = if no_progress_bar { + None + } else { + let progress_bar = ProgressBar::new(total_size); + progress_bar.set_style(ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .progress_chars("#>-")); + Some(progress_bar) + }; - panic!("Failed to retrieve the asset size for latest release"); + let to_io_error = + |e| std::io::Error::new(std::io::ErrorKind::Other, format!("Reqwest error: {e}")); + + let mut source = reqwest::Client::new() + .get(asset_download_url) + .send() + .await + .map_err(to_io_error)?; + + let mut dest = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&tmp.as_path()) + .await?; + + while let Some(chunk) = source.chunk().await.map_err(to_io_error)? { + dest.write_all(&chunk).await?; + + if let Some(ref mut progress_bar) = maybe_progress_bar { + progress_bar.inc(chunk.len() as u64); + } } + + Ok(DownloadResult::Success(tmp)) } diff --git a/crates/upgrade/src/lib.rs b/crates/upgrade/src/lib.rs index 065cc1256..5e0337398 100644 --- a/crates/upgrade/src/lib.rs +++ b/crates/upgrade/src/lib.rs @@ -1,244 +1,7 @@ -//! This crate provides the features of upgrading the maple executable. +//! This crate implements the functionality of downloading an asset from GitHub release +//! and provides the feature of upgrading the maple executable on top of it. mod github; +mod maple_upgrade; -use crate::github::{asset_download_url, asset_name, retrieve_asset_size, retrieve_latest_release}; -use indicatif::{ProgressBar, ProgressStyle}; -use std::path::PathBuf; -use tokio::io::AsyncWriteExt; - -/// This command is only invoked when user uses the prebuilt binary, more specifically, the -/// executable runs from `vim-clap/bin/maple`. -#[derive(Debug, Clone)] -pub struct Upgrade { - /// Download if the local version mismatches the latest remote version. - pub download: bool, - /// Disable the downloading progress_bar - pub no_progress_bar: bool, -} - -impl Upgrade { - pub fn new(download: bool, no_progress_bar: bool) -> Self { - Self { - download, - no_progress_bar, - } - } - - pub async fn run(&self, local_tag: &str) -> std::io::Result<()> { - println!("Retrieving the latest remote release info..."); - let latest_release = retrieve_latest_release().await?; - let latest_tag = latest_release.tag_name; - let latest_version = extract_remote_version_number(&latest_tag); - let local_version = extract_local_version_number(local_tag); - - if latest_version != local_version { - if self.download { - println!("New maple release {latest_tag} is available, downloading...",); - - let temp_file = download_prebuilt_binary(&latest_tag, self.no_progress_bar).await?; - - // Only tries to upgrade if using the prebuilt binary, i.e., `bin/maple`. - let bin_path = get_binary_path()?; - - // Move the downloaded binary to bin/maple - std::fs::rename(temp_file, bin_path)?; - - println!("Latest version {latest_tag} download completed"); - } else { - match asset_download_url(&latest_tag) { - Some(url) => { - println!("New maple release {latest_tag} is available, please download it from {url} or rerun with --download flag."); - } - None => { - println!("New maple release {latest_tag} is available, but no prebuilt binary provided for your platform"); - } - } - } - } else { - println!("No newer prebuilt binary release, current maple version: {local_version}"); - } - - Ok(()) - } -} - -/// The prebuilt binary is put at bin/maple. -fn get_binary_path() -> std::io::Result> { - use std::io::{Error, ErrorKind}; - - let exe_dir = std::env::current_exe()?; - let bin_dir = exe_dir.parent().ok_or_else(|| { - Error::new( - ErrorKind::NotFound, - "Parent directory of current executable not found", - ) - })?; - - if !bin_dir.ends_with("bin") { - return Err(Error::new( - ErrorKind::Other, - "Current executable is not from bin/***", - )); - } - - let bin_path = if cfg!(windows) { - bin_dir.join("maple.exe") - } else { - bin_dir.join("maple") - }; - - Ok(bin_path) -} - -/// Extracts the number of version from tag name, e.g., returns 13 out of the tag `v0.13`. -#[inline] -fn extract_remote_version_number(remote_tag: &str) -> u32 { - remote_tag - .split('.') - .nth(1) - .and_then(|s| s.parse().ok()) - .expect("Couldn't extract remote version") -} - -/// local: "v0.13-4-g58738c0" -#[inline] -fn extract_local_version_number(local_tag: &str) -> u32 { - let tag = local_tag.split('-').next().expect("Invalid local tag"); - extract_remote_version_number(tag) -} - -#[cfg(unix)] -fn set_executable_permission>(path: P) -> std::io::Result<()> { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(path.as_ref())?.permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(path.as_ref(), perms)?; - Ok(()) -} - -/// Downloads the latest remote release binary to a temp file. -/// -/// # Arguments -/// -/// - `version`: "v0.13" -async fn download_prebuilt_binary( - version: &str, - no_progress_bar: bool, -) -> std::io::Result { - let binary_unavailable = || { - std::io::Error::new( - std::io::ErrorKind::Other, - "No available prebuilt binary for this platform", - ) - }; - - let asset_name = asset_name().ok_or_else(binary_unavailable)?; - - let mut tmp = std::env::temp_dir(); - tmp.push(format!("{version}-{asset_name}")); - - let total_size = retrieve_asset_size(asset_name, version).await?; - - // Check if there is a partially downloaded binary before. - if tmp.is_file() { - let metadata = std::fs::metadata(&tmp)?; - if metadata.len() == total_size { - println!("{} has alreay been downloaded", tmp.display()); - return Ok(tmp); - } else { - std::fs::remove_file(&tmp)?; - } - } - - let mut maybe_progress_bar = if no_progress_bar { - None - } else { - let progress_bar = ProgressBar::new(total_size); - progress_bar.set_style(ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") - .progress_chars("#>-")); - Some(progress_bar) - }; - - let to_io_error = - |e| std::io::Error::new(std::io::ErrorKind::Other, format!("Reqwest error: {e}")); - - let download_url = asset_download_url(version).ok_or_else(binary_unavailable)?; - let mut source = reqwest::Client::new() - .get(download_url) - .send() - .await - .map_err(to_io_error)?; - - let mut dest = tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&tmp.as_path()) - .await?; - - while let Some(chunk) = source.chunk().await.map_err(to_io_error)? { - dest.write_all(&chunk).await?; - - if let Some(ref mut progress_bar) = maybe_progress_bar { - progress_bar.inc(chunk.len() as u64); - } - } - - #[cfg(unix)] - set_executable_permission(&tmp)?; - - println!("Download of '{}' has been completed.", tmp.display()); - - Ok(tmp) -} - -#[cfg(test)] -mod tests { - use super::*; - - pub fn is_commit_associated_with_a_tag() -> bool { - let output = std::process::Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .expect("Failed to find HEAD commit"); - let commit_id = String::from_utf8_lossy(&output.stdout); - - std::process::Command::new("git") - .args(["describe", "--tags", "--exact-match", commit_id.trim()]) - .status() - .map(|exit_status| exit_status.success()) - .unwrap_or(false) - } - - #[test] - fn test_extract_version_number() { - let tag = "v0.13-4-g58738c0"; - assert_eq!(13u32, extract_local_version_number(tag)); - let tag = "v0.13"; - assert_eq!(13u32, extract_local_version_number(tag)); - } - - #[tokio::test] - async fn test_download_prebuilt_binary() { - // Ignore this test when the commit is associated with a tag as the binary is possibly not - // yet able to be uploaded to the release page. - if is_commit_associated_with_a_tag() { - return; - } - - for _i in 0..20 { - if let Ok(latest_tag) = retrieve_latest_release().await.map(|r| r.tag_name) { - download_prebuilt_binary(&latest_tag, true) - .await - .unwrap_or_else(|err| panic!( - "Failed to download the prebuilt binary for {latest_tag:?} into a tempfile: {err:?}" - )); - return; - } - tokio::time::sleep(std::time::Duration::from_millis(300)).await; - } - - panic!("Failed to download the prebuilt binary of latest release"); - } -} +pub use maple_upgrade::Upgrade; diff --git a/crates/upgrade/src/maple_upgrade.rs b/crates/upgrade/src/maple_upgrade.rs new file mode 100644 index 000000000..dbc1d10a1 --- /dev/null +++ b/crates/upgrade/src/maple_upgrade.rs @@ -0,0 +1,276 @@ +use crate::github::{ + download_asset_file, latest_github_release, request, DownloadResult, GitHubRelease, +}; +use std::path::PathBuf; + +fn asset_name() -> Option<&'static str> { + if cfg!(target_os = "macos") { + if cfg!(target_arch = "x86_64") { + Some("maple-x86_64-apple-darwin") + } else if cfg!(target_arch = "aarch64") { + Some("maple-aarch64-apple-darwin") + } else { + None + } + } else if cfg!(target_os = "linux") { + if cfg!(target_arch = "x86_64") { + Some("maple-x86_64-unknown-linux-musl") + } else if cfg!(target_arch = "aarch64") { + Some("maple-aarch64-unknown-linux-gnu") + } else { + None + } + } else if cfg!(target_os = "windows") { + Some("maple-x86_64-pc-windows-msvc") + } else { + None + } +} + +fn maple_asset_download_url(version: &str) -> Option { + asset_name().map(|asset_name| { + format!("https://github.com/liuchengxu/vim-clap/releases/download/{version}/{asset_name}",) + }) +} + +async fn fetch_asset_size(asset_name: &str, tag: &str) -> std::io::Result { + let url = format!("https://api.github.com/repos/liuchengxu/vim-clap/releases/tags/{tag}"); + let release: GitHubRelease = request(&url, "liuchengxu").await?; + + release + .assets + .iter() + .find(|x| x.name == asset_name) + .map(|x| x.size) + .ok_or_else(|| panic!("Can not find the asset {asset_name} in given release {tag}")) +} + +/// This command is only invoked when user uses the prebuilt binary, more specifically, the +/// executable runs from `vim-clap/bin/maple`. +#[derive(Debug, Clone)] +pub struct Upgrade { + /// Download if the local version mismatches the latest remote version. + pub download: bool, + /// Disable the downloading progress_bar + pub no_progress_bar: bool, +} + +impl Upgrade { + pub fn new(download: bool, no_progress_bar: bool) -> Self { + Self { + download, + no_progress_bar, + } + } + + pub async fn run(&self, local_tag: &str) -> std::io::Result<()> { + println!("Retrieving the latest remote release info..."); + let latest_release = latest_github_release("liuchengxu", "vim-clap").await?; + let latest_tag = latest_release.tag_name; + let latest_version = extract_remote_version_number(&latest_tag); + let local_version = extract_local_version_number(local_tag); + + if latest_version != local_version { + if self.download { + println!("New maple release {latest_tag} is available, downloading...",); + + let temp_file = download_prebuilt_binary(&latest_tag, self.no_progress_bar).await?; + + // Only tries to upgrade if using the prebuilt binary, i.e., `bin/maple`. + let bin_path = get_binary_path()?; + + // Move the downloaded binary to bin/maple + std::fs::rename(temp_file, bin_path)?; + + println!("Latest version {latest_tag} download completed"); + } else { + match maple_asset_download_url(&latest_tag) { + Some(url) => { + println!("New maple release {latest_tag} is available, please download it from {url} or rerun with --download flag."); + } + None => { + println!("New maple release {latest_tag} is available, but no prebuilt binary provided for your platform"); + } + } + } + } else { + println!("No newer prebuilt binary release, current maple version: {local_version}"); + } + + Ok(()) + } +} + +/// The prebuilt binary is put at bin/maple. +fn get_binary_path() -> std::io::Result> { + use std::io::{Error, ErrorKind}; + + let exe_dir = std::env::current_exe()?; + let bin_dir = exe_dir.parent().ok_or_else(|| { + Error::new( + ErrorKind::NotFound, + "Parent directory of current executable not found", + ) + })?; + + if !bin_dir.ends_with("bin") { + return Err(Error::new( + ErrorKind::Other, + "Current executable is not from bin/***", + )); + } + + let bin_path = if cfg!(windows) { + bin_dir.join("maple.exe") + } else { + bin_dir.join("maple") + }; + + Ok(bin_path) +} + +/// Extracts the number of version from tag name, e.g., returns 13 out of the tag `v0.13`. +#[inline] +fn extract_remote_version_number(remote_tag: &str) -> u32 { + remote_tag + .split('.') + .nth(1) + .and_then(|s| s.parse().ok()) + .expect("Couldn't extract remote version") +} + +/// local: "v0.13-4-g58738c0" +#[inline] +fn extract_local_version_number(local_tag: &str) -> u32 { + let tag = local_tag.split('-').next().expect("Invalid local tag"); + extract_remote_version_number(tag) +} + +#[cfg(unix)] +fn set_executable_permission>(path: P) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(path.as_ref())?.permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(path.as_ref(), perms)?; + Ok(()) +} + +/// Downloads the latest remote release binary to a temp file. +/// +/// # Arguments +/// +/// - `version`: "v0.13" +async fn download_prebuilt_binary( + version: &str, + no_progress_bar: bool, +) -> std::io::Result { + let binary_unavailable = || { + std::io::Error::new( + std::io::ErrorKind::Other, + "No available prebuilt binary for this platform", + ) + }; + + let asset_name = asset_name().ok_or_else(binary_unavailable)?; + let total_size = fetch_asset_size(asset_name, version).await?; + let download_url = maple_asset_download_url(version).ok_or_else(binary_unavailable)?; + + let tmp = match download_asset_file( + version, + asset_name, + total_size, + &download_url, + no_progress_bar, + ) + .await? + { + DownloadResult::Existed(tmp) => { + println!("{} has already been downloaded", tmp.display()); + tmp + } + DownloadResult::Success(tmp) => { + println!("Download of '{}' has been completed.", tmp.display()); + tmp + } + }; + + #[cfg(unix)] + set_executable_permission(&tmp)?; + + Ok(tmp) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn is_commit_associated_with_a_tag() -> bool { + let output = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("Failed to find HEAD commit"); + let commit_id = String::from_utf8_lossy(&output.stdout); + + std::process::Command::new("git") + .args(["describe", "--tags", "--exact-match", commit_id.trim()]) + .status() + .map(|exit_status| exit_status.success()) + .unwrap_or(false) + } + + #[test] + fn test_extract_version_number() { + let tag = "v0.13-4-g58738c0"; + assert_eq!(13u32, extract_local_version_number(tag)); + let tag = "v0.13"; + assert_eq!(13u32, extract_local_version_number(tag)); + } + + #[tokio::test] + async fn test_retrieve_asset_size() { + if is_commit_associated_with_a_tag() { + return; + } + + for _i in 0..20 { + if let Ok(latest_tag) = latest_github_release("liuchengxu", "vim-clap") + .await + .map(|r| r.tag_name) + { + fetch_asset_size(asset_name().unwrap(), &latest_tag) + .await + .expect("Failed to retrieve the asset size for latest release"); + return; + } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + + panic!("Failed to retrieve the asset size for latest release"); + } + + #[tokio::test] + async fn test_download_prebuilt_binary() { + // Ignore this test when the commit is associated with a tag as the binary is possibly not + // yet able to be uploaded to the release page. + if is_commit_associated_with_a_tag() { + return; + } + + for _i in 0..20 { + if let Ok(latest_tag) = latest_github_release("liuchengxu", "vim-clap") + .await + .map(|r| r.tag_name) + { + download_prebuilt_binary(&latest_tag, true) + .await + .unwrap_or_else(|err| panic!( + "Failed to download the prebuilt binary for {latest_tag:?} into a tempfile: {err:?}" + )); + return; + } + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + } + + panic!("Failed to download the prebuilt binary of latest release"); + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index fd8770a0f..514cab31d 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -8,5 +8,3 @@ edition.workspace = true bytecount = { workspace = true } memchr = { workspace = true } simdutf8 = { workspace = true } - -types = { workspace = true } diff --git a/crates/xtask/src/release.rs b/crates/xtask/src/release.rs index 319eb4300..6ccb340b7 100644 --- a/crates/xtask/src/release.rs +++ b/crates/xtask/src/release.rs @@ -102,6 +102,7 @@ pub fn run(sh: &Shell, dry_run: bool, project_root: PathBuf) -> Result<()> { Ok(()) } +// Find the target_line in the file and update the line by incrementing the version. fn bump_version( sh: &Shell, path: PathBuf, diff --git a/docs/src/plugins/config.md b/docs/src/plugins/config.md index 70c1e5a78..92dcc0bdb 100644 --- a/docs/src/plugins/config.md +++ b/docs/src/plugins/config.md @@ -65,13 +65,18 @@ enable = false [plugin.lsp] # Whether to enable this plugin. enable = false -# Whether to include the declaration when invoking goto-reference. +# Whether to include the declaration when invoking `ClapAction lsp.reference`. include-declaration = false +# Specify the list of filetypes for which the lsp plugin will be disabled. +# +# If a filetype is included in this list, the Language Server Protocol (LSP) plugin +# will not be activated for files with that particular type. +filetype-blocklist = [] # Specifies custom languages that are not built into vim-clap. # -# If a language is not included in the default languages supported by vim-clap, -# you can specify it here. Note that for languages not listed in the default -# configuration (check out the full list of supported languages in `languages.toml`), +# This config allows to define a new language or override the default value +# of the built-in language config. Note that if you are defining a new language, +# (check out the full list of supported languages by default in `languages.toml`), # you need to provide associated language server configurations as well. # # # Example @@ -79,13 +84,12 @@ include-declaration = false # ```toml # [[plugin.lsp.language]] # name = "erlang" -# filetype = ["erlang"] +# file-types = ["erlang"] # root-markers = ["rebar.config"] # language-servers = ["erlang-ls"] # # [plugin.lsp.language-server.erlang-ls] # command = "erlang_ls" -# args = ["--transport", "stdio"] # ``` language = [] diff --git a/docs/src/plugins/plugins.md b/docs/src/plugins/plugins.md index 5ac6f59cc..d265e68a1 100644 --- a/docs/src/plugins/plugins.md +++ b/docs/src/plugins/plugins.md @@ -49,6 +49,10 @@ By default this plugin utilizes `Normal` guibg as the primary color. It then lig and darkens it for `ClapCursorWordTwins`. You can manually adjust them in case the default highlights does not meet your expectations. +## diagnostics + +This plugin does not do any substantial work but used to provide an interface to interact with the diagnostics provided by the linter and lsp plugin. + ## git ```toml diff --git a/languages.toml b/languages.toml index 216ef3742..ba2420b54 100644 --- a/languages.toml +++ b/languages.toml @@ -11,8 +11,10 @@ line-comments = [";"] [[language]] name = "cpp" +file-types = ["cpp"] file-extensions = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] line-comments = ["//"] +language-servers = ["clangd"] [[language]] name = "elisp" @@ -35,7 +37,7 @@ name = "go" file-types = ["go"] line-comments = ["//"] root-markers = ["go.mod"] -language-servers = [ "gopls" ] +language-servers = ["gopls"] [[language]] name = "javascript" @@ -85,7 +87,7 @@ name = "rust" file-types = ["rust"] line-comments = ["//", "///", "//!"] root-markers = ["Cargo.toml", "Cargo.lock"] -language-servers = [ "rust-analyzer" ] +language-servers = ["rust-analyzer"] [[language]] name = "tex" @@ -194,12 +196,12 @@ typst-lsp = { command = "typst-lsp" } command = "ansible-language-server" args = ["--stdio"] -[language-server.lua-language-server] -command = "lua-language-server" - [language-server.gopls] command = "gopls" +[language-server.lua-language-server] +command = "lua-language-server" + [language-server.rust-analyzer] command = "rust-analyzer" diff --git a/plugin/clap.vim b/plugin/clap.vim index c2937a7ef..8e6cdc9a5 100644 --- a/plugin/clap.vim +++ b/plugin/clap.vim @@ -52,7 +52,7 @@ augroup VimClap autocmd BufDelete * call s:OnBufDelete(+expand('')) autocmd BufWinEnter,WinEnter * let g:__clap_buffers[bufnr('')] = reltimefloat(reltime()) - autocmd BufAdd * call clap#client#notify('__note_recent_files', [+expand('')]) + autocmd BufAdd * call clap#client#notify('__noteRecentFiles', [+expand('')]) if get(g:, 'clap_plugin_experimental', 0) autocmd InsertEnter * call clap#client#notify('InsertEnter', [+expand('')]) @@ -71,7 +71,7 @@ augroup VimClap " Create `clap_actions` provider so that it's convenient to interact with the plugins later. let g:clap_provider_clap_actions = get(g:, 'clap_provider_clap_actions', { \ 'source': { -> get(g:, 'clap_actions', []) }, - \ 'sink': { line -> clap#client#notify(line, []) }, + \ 'sink': { line -> clap#client#notify(line, [ g:clap.start.bufnr, g:clap.start.old_pos[1], g:clap.start.old_pos[2] ] ) }, \ 'mode': 'quick_pick', \ })