Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP (Blocked): Indentation guides #720

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Breaking changes:
- Removed `lsp-connect` experimental command.

Additions:
- New `lsp-indent-guides-enable` command enables indentation guides rendering.
- `lsp-document-symbol` no longer renders the same filename in every single line. Commands like `jump-next` and `<ret>` still work as before.
- `lsp_hover_max_diagnostic_lines` now defaults to 20 which limits the diagnostic lines in the hover box.
- Various improvements to the compatibility with old Kakoune.
Expand Down
3 changes: 3 additions & 0 deletions README.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ hook global WinSetOption filetype=rust %{
* `lsp-next-symbol` and `lsp-previous-symbol` command to go to the buffer's next and current/previous symbol.
* `lsp-hover-next-symbol` and `lsp-hover-previous-symbol` to show hover of the buffer's next and current/previous symbol.
* `lsp-rename <new_name>` and `lsp-rename-prompt` commands to rename the symbol under the main cursor.
* `lsp-indent-guides-enable` command to render indentation guides.
** `lsp_indent_guides_characters` option to customize indentation guide characters, default is `│ ┆ ┊ ⸽`.
** `lsp_indent_guides_skip` option to customize the scope level to render indentation guides from, default is `1`.
* Breadcrumbs in the modeline indicating the symbol around the main cursor, like (`somemodule > someclass > somefunction`).
** To implement this, kakoune-lsp adds `%opt{lsp_modeline} ` to the front of your global `modelinefmt` at load time.
* An hourglass character (⌛) in the modeline whenever the language server indicates it's busy.
Expand Down
39 changes: 39 additions & 0 deletions rc/lsp.kak
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ set-face global ReferenceBind +u@Reference
# Face for inlay hints.
set-face global InlayHint cyan+d
set-face global InlayCodeLens cyan+d
# Face for indent guides
set-face global IndentGuide +f@comment
set-face global IndentGuideFocused +f@MatchingChar

# Options for tuning LSP behaviour.

Expand Down Expand Up @@ -249,6 +252,9 @@ info=$lsp_info \
# If you want to see only hover info, try
# set-option global lsp_show_hover_format 'printf %s "${lsp_info}"'

declare-option -docstring "Number of indent levels to skip" int lsp_indent_guides_skip 1
declare-option -docstring "Indent guides characters" str-list lsp_indent_guides_characters "│" "┆" "┊" "⸽"

# Callback functions. May override these.

declare-option -hidden str lsp_code_action_indicator
Expand Down Expand Up @@ -454,6 +460,7 @@ declare-option -hidden range-specs cquery_semhl
declare-option -hidden int lsp_timestamp -1
declare-option -hidden range-specs lsp_references
declare-option -hidden range-specs lsp_semantic_tokens
declare-option -hidden range-specs lsp_indent_guides
declare-option -hidden range-specs lsp_inlay_hints
declare-option -hidden line-specs lsp_inlay_code_lenses
declare-option -hidden line-specs lsp_code_lenses 0 '0| '
Expand Down Expand Up @@ -1533,6 +1540,38 @@ position_line = $kak_cursor_line
" | eval "${kak_opt_lsp_cmd} --request") > /dev/null 2>&1 < /dev/null & }
}

define-command -hidden lsp-indent-guides-request -docstring "request updating lsp_indent_guides for the buffer" %{
nop %sh{
eval set -- "$kak_quoted_opt_lsp_indent_guides_characters"
# also remove the trailing comma
indent_guides_characters="[ $(printf "'%s', " "$@" | rev | cut -c2- | rev) ]"

(printf %s "
session = \"${kak_session}\"
buffile = \"${kak_buffile}\"
filetype = \"${kak_opt_filetype}\"
version = ${kak_timestamp:-0}
method = \"kakoune/indent-guides\"
$([ -z ${kak_hook_param+x} ] || echo hook = true)
[params]
skip = $kak_opt_lsp_indent_guides_skip
characters = $indent_guides_characters
position_line = $kak_cursor_line
" | eval "${kak_opt_lsp_cmd} --request") > /dev/null 2>&1 < /dev/null & }
}

define-command lsp-indent-guides-enable -params 1 -docstring "lsp-indent-guides-enable <scope>: enable indent guides for <scope>" %{
add-highlighter -override "%arg{1}/lsp_indent_guides" replace-ranges lsp_indent_guides
hook -group lsp-indent-guides %arg{1} BufReload .* lsp-indent-guides-request
hook -group lsp-indent-guides %arg{1} NormalIdle .* lsp-indent-guides-request
hook -group lsp-indent-guides %arg{1} InsertIdle .* lsp-indent-guides-request
} -shell-script-candidates %{ printf '%s\n' buffer global window }

define-command lsp-indent-guides-disable -params 1 -docstring "lsp-indent-guides-disable <scope>: disable indent guides for <scope>" %{
remove-highlighter "%arg{1}/lsp_indent_guides"
remove-hooks %arg{1} lsp-indent-guides
} -shell-script-candidates %{ printf '%s\n' buffer global window }

define-command -hidden lsp-inlay-hints -docstring "lsp-inlay-hints: request inlay hints" %{
declare-option -hidden int lsp_inlay_hints_timestamp -1
nop %sh{
Expand Down
3 changes: 3 additions & 0 deletions src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ fn dispatch_editor_request(request: EditorRequest, ctx: &mut Context) {
"kakoune/breadcrumbs" => {
document_symbol::breadcrumbs(meta, params, ctx);
}
"kakoune/indent-guides" => {
document_symbol::indent_guides(meta, params, ctx);
}
"kakoune/next-or-previous-symbol" => {
document_symbol::next_or_prev_symbol(meta, params, ctx);
}
Expand Down
200 changes: 198 additions & 2 deletions src/language_features/document_symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::language_features::hover::editor_hover;
use crate::markup::escape_kakoune_markup;
use crate::position::{
get_kakoune_position_with_fallback, get_kakoune_range, get_kakoune_range_with_fallback,
get_lsp_position, kakoune_position_to_lsp, lsp_range_to_kakoune, parse_kakoune_range,
get_lsp_position, kakoune_position_to_lsp, lsp_position_to_kakoune, lsp_range_to_kakoune,
parse_kakoune_range,
};
use crate::types::*;
use crate::util::*;
Expand All @@ -15,13 +16,208 @@ use lsp_types::request::*;
use lsp_types::*;
use serde::Deserialize;
use std::any::TypeId;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryInto;
use std::fmt;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use url::Url;

pub fn indent_guides(meta: EditorMeta, editor_params: EditorParams, ctx: &mut Context) {
let eligible_servers: Vec<_> = ctx
.language_servers
.iter()
.filter(|srv| attempt_server_capability(*srv, &meta, CAPABILITY_DOCUMENT_SYMBOL))
.collect();
let req_params = eligible_servers
.into_iter()
.map(|(server_name, _)| {
(
server_name.clone(),
vec![DocumentSymbolParams {
text_document: TextDocumentIdentifier {
uri: Url::from_file_path(&meta.buffile).unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
}],
)
})
.collect();
let params = IndentGuidesParams::deserialize(editor_params).unwrap();
ctx.call::<DocumentSymbolRequest, _>(
meta,
RequestParams::Each(req_params),
move |ctx: &mut Context, meta, results| {
match results.into_iter().find(|(_, v)| v.is_some()) {
Some((server_name, Some(DocumentSymbolResponse::Nested(symbols))))
if !symbols.is_empty() =>
{
let server = &ctx.language_servers[&server_name];
let mut line_indents = LineIndents::default();
indent_guides_calc_range_specs(
&symbols,
&params,
&meta,
ctx,
server,
0,
&mut line_indents,
);
let range_specs = line_indents.render();
let version = meta.version;
let command =
formatdoc!("set-option buffer lsp_indent_guides {version} {range_specs}");
let command = format!(
"evaluate-commands -buffer {} -- {}",
editor_quote(&meta.buffile),
editor_quote(&command)
);
ctx.exec(meta, command);
}
Some((_, Some(DocumentSymbolResponse::Nested(_)))) => {
return;
}
Some((_, Some(DocumentSymbolResponse::Flat(_)))) => {
return;
}
Some((_, None)) => {
return;
}
None => {
return;
}
};
},
);
}

#[derive(Default, Debug)]
struct LineIndents {
repr: BTreeMap<usize, BTreeMap<usize, (String, bool)>>,
}

impl LineIndents {
fn add(self: &mut LineIndents, line: usize, column: usize, char: String, focused: bool) {
match self.repr.get_mut(&line) {
Some(line_repr) => {
line_repr.insert(column, (char, focused));
}
None => {
let mut line_repr = BTreeMap::default();
line_repr.insert(column, (char, focused));
self.repr.insert(line, line_repr);
}
}
}

fn render(self: &LineIndents) -> String {
let mut accumulator = String::default();
for (line, line_repr) in &self.repr {
let max_column = (*line_repr.last_key_value().unwrap().0) as i32;
let mut spec = String::default();
spec.push_str(format!("{}.1+{}|", line, 0).as_str());
for c in (1)..=max_column {
match line_repr.get(&(c as usize)) {
Some((indent_character, focused)) => {
let face = if *focused {
"IndentGuideFocused"
} else {
"IndentGuide"
};
let tuple = format!(
"{{{}}}{}{{default}}",
face,
escape_tuple_element(indent_character)
);
spec.push_str(tuple.as_str());
}
None => {
spec.push(' ');
}
}
}
let spec = editor_quote(spec.as_str());
accumulator.push_str(spec.as_str());
accumulator.push(' ');
}
return accumulator;
}
}

fn indent_guides_calc_range_specs(
symbols: &[DocumentSymbol],
params: &IndentGuidesParams,
meta: &EditorMeta,
ctx: &Context,
server: &ServerSettings,
level: usize,
line_indents: &mut LineIndents,
) {
for symbol in symbols {
let should_skip = level < params.skip;

let mut filename_path = PathBuf::default();
let filename = symbol_filename(meta, symbol, &mut filename_path).to_string();
let range = get_kakoune_range(server, filename.as_str(), &symbol.range, ctx).unwrap();

let children = symbol.children();

if !should_skip {
let is_not_in_child = children
.iter()
.find(|child| {
let mut filename_path = PathBuf::default();
let filename = symbol_filename(meta, *child, &mut filename_path).to_string();
let range =
get_kakoune_range(server, filename.as_str(), &child.range, ctx).unwrap();
range.start.line <= params.position_line
&& params.position_line <= range.end.line
})
.is_none();

let is_inside =
range.start.line <= params.position_line && params.position_line <= range.end.line;
let is_focused = is_not_in_child && is_inside;

let adjusted_level = level - params.skip;
let indent_character = params
.characters
.get(adjusted_level)
.or(params.characters.last())
.unwrap();

let document = ctx.documents.get(filename.as_str()).unwrap();
let rope = &document.text;
let offset_encoding = server.offset_encoding;
// Ignoring one-line indents.
let symbol_line =
lsp_position_to_kakoune(&symbol.selection_range.end, rope, offset_encoding).line;
if range.end.line - symbol_line > 1 {
// We want to ignore putting indentation on signatures and comments,
// thus starting from `selection_range`.
for line in symbol_line..=(range.end.line) {
line_indents.add(
line as usize,
range.start.column as usize,
indent_character.to_string(),
is_focused,
);
}
}
};
indent_guides_calc_range_specs(
children,
params,
meta,
ctx,
server,
level + 1,
line_indents,
);
}
}

pub fn text_document_document_symbol(meta: EditorMeta, ctx: &mut Context) {
let eligible_servers: Vec<_> = ctx
.language_servers
Expand Down
8 changes: 8 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ pub struct BreadcrumbsParams {
pub position_line: u32,
}

#[derive(Clone, Default, Deserialize, Debug)]
#[serde(default)]
pub struct IndentGuidesParams {
pub skip: usize,
pub characters: Vec<String>,
pub position_line: u32,
}

#[derive(Clone, Deserialize, Debug)]
pub struct GotoSymbolParams {
pub goto_symbol: Option<String>,
Expand Down
Loading