feat(lsp): implement syntax aware selection (#1353)

This commit is contained in:
Paul Delafosse 2022-09-27 20:40:23 +02:00 committed by GitHub
parent 7213ae1dc8
commit 42a23afd35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 547 additions and 71 deletions

View File

@ -138,6 +138,14 @@ key = "esc"
command = "clear_search"
when = "search_focus"
[[keymaps]]
key = "ctrl+shift+up"
command = "select_next_syntax_item"
[[keymaps]]
key = "ctrl+shift+down"
command = "select_previous_syntax_item"
[[keymaps]]
key = "ctrl+m"
command = "list.select"

View File

@ -304,6 +304,10 @@ pub enum FocusCommand {
Rename,
#[strum(serialize = "confirm_rename")]
ConfirmRename,
#[strum(serialize = "select_next_syntax_item")]
SelectNextSyntaxItem,
#[strum(serialize = "select_previous_syntax_item")]
SelectPreviousSyntaxItem,
}
#[derive(

View File

@ -274,6 +274,23 @@ pub fn yank(&self, buffer: &Buffer) -> RegisterData {
RegisterData { content, mode }
}
/// Return the current selection start and end position for a
/// Single cursor selection
pub fn get_selection(&self) -> Option<(usize, usize)> {
match &self.mode {
CursorMode::Visual {
start,
end,
mode: _,
} => Some((*start, *end)),
CursorMode::Insert(selection) => selection
.regions()
.first()
.map(|region| (region.start, region.end)),
_ => None,
}
}
pub fn set_offset(&mut self, offset: usize, modify: bool, new_cursor: bool) {
match &self.mode {
CursorMode::Normal(old_offset) => {

View File

@ -18,7 +18,7 @@
use lsp_types::{
CodeActionOrCommand, CodeActionResponse, CompletionItem, CompletionResponse,
InlayHint, Location, Position, ProgressParams, PublishDiagnosticsParams,
TextEdit, WorkspaceEdit,
SelectionRange, TextEdit, WorkspaceEdit,
};
use serde_json::Value;
use strum::{self, EnumMessage, IntoEnumIterator};
@ -31,6 +31,7 @@
use crate::editor::{EditorPosition, Line, LineCol};
use crate::menu::MenuKind;
use crate::rich_text::RichText;
use crate::selection_range::SelectionRangeDirection;
use crate::update::ReleaseInfo;
use crate::{
data::{EditorTabChild, SplitContent},
@ -722,6 +723,18 @@ pub enum LapceUICommand {
CopyPath(PathBuf),
CopyRelativePath(PathBuf),
SetLanguage(String),
ApplySelectionRange {
buffer_id: BufferId,
rev: u64,
direction: SelectionRangeDirection,
},
StoreSelectionRangeAndApply {
buffer_id: BufferId,
rev: u64,
current_selection: Option<(usize, usize)>,
ranges: Vec<SelectionRange>,
direction: SelectionRangeDirection,
},
/// An item in a list was chosen
/// This is typically targeted at the widget which contains the list

View File

@ -44,6 +44,7 @@
Interval, Rope, RopeDelta, Transformer,
};
use crate::selection_range::SelectionRangeDirection;
use crate::{
command::{InitBufferContentCb, LapceUICommand, LAPCE_UI_COMMAND},
config::{Config, LapceTheme},
@ -52,6 +53,7 @@
find::{Find, FindProgress},
history::DocumentHistory,
proxy::LapceProxy,
selection_range::SyntaxSelectionRanges,
settings::SettingsValueKind,
};
@ -416,6 +418,7 @@ pub struct Document {
pub code_actions: im::HashMap<usize, CodeActionResponse>,
pub inlay_hints: Option<Spans<InlayHint>>,
pub diagnostics: Option<Arc<Vec<EditorDiagnostic>>>,
pub syntax_selection_range: Option<SyntaxSelectionRanges>,
pub find: Rc<RefCell<Find>>,
find_progress: Rc<RefCell<FindProgress>>,
pub event_sink: ExtEventSink,
@ -462,6 +465,7 @@ pub fn new(
find_progress: Rc::new(RefCell::new(FindProgress::Ready)),
event_sink,
proxy,
syntax_selection_range: None,
}
}
@ -2677,4 +2681,24 @@ pub fn sticky_headers(&self, line: usize) -> Option<Vec<usize>> {
self.sticky_headers.borrow_mut().insert(line, lines.clone());
lines
}
pub fn change_syntax_selection(
&mut self,
direction: SelectionRangeDirection,
) -> Option<Selection> {
if let Some(selections) = self.syntax_selection_range.as_mut() {
match direction {
SelectionRangeDirection::Next => selections.next_range(),
SelectionRangeDirection::Previous => selections.previous_range(),
}
.map(|range| {
let start = self.buffer.offset_of_position(&range.start);
let end = self.buffer.offset_of_position(&range.end);
selections.last_known_selection = Some((start, end));
Selection::region(start, end)
})
} else {
None
}
}
}

View File

@ -21,6 +21,7 @@
use crate::palette::PaletteData;
use crate::proxy::path_from_url;
use crate::rename::RenameData;
use crate::selection_range::SelectionRangeDirection;
use crate::{
command::{
EnsureVisiblePosition, InitBufferContent, LapceUICommand, LAPCE_UI_COMMAND,
@ -125,6 +126,7 @@ fn init_buffer_content_cmd(
/// (If you want to jump to the very first character then use `LineCol` with column set to 0)
#[derive(Debug, Clone, Copy)]
pub struct Line(pub usize);
impl EditorPosition for Line {
fn to_utf8_offset(&self, buffer: &Buffer) -> usize {
buffer.first_non_blank_character_on_line(self.0.saturating_sub(1))
@ -153,6 +155,7 @@ pub struct LineCol {
pub line: usize,
pub column: usize,
}
impl EditorPosition for LineCol {
fn to_utf8_offset(&self, buffer: &Buffer) -> usize {
buffer.offset_of_line_col(self.line, self.column)
@ -2222,11 +2225,77 @@ fn run_focus_command(
Target::Widget(self.rename.view_id),
));
}
SelectNextSyntaxItem => {
self.run_selection_range_command(ctx, SelectionRangeDirection::Next)
}
SelectPreviousSyntaxItem => self
.run_selection_range_command(ctx, SelectionRangeDirection::Previous),
_ => return CommandExecuted::No,
}
CommandExecuted::Yes
}
fn run_selection_range_command(
&mut self,
ctx: &mut EventCtx,
direction: SelectionRangeDirection,
) {
let offset = self.editor.cursor.offset();
if let BufferContent::File(path) = self.doc.content() {
let rev = self.doc.buffer().rev();
let buffer_id = self.doc.id();
let event_sink = ctx.get_external_handle();
let current_selection = self.editor.cursor.get_selection();
match &self.doc.syntax_selection_range {
// If the cached selection range match current revision, no need to call the
// LSP server, we ca apply it right now
Some(selection_range)
if selection_range.match_request(
buffer_id,
rev,
current_selection,
) =>
{
let _ = event_sink.submit_command(
LAPCE_UI_COMMAND,
LapceUICommand::ApplySelectionRange {
rev,
buffer_id,
direction,
},
Target::Auto,
);
}
// Otherwise, ask the LSP server for `textDocument/selectionRange`
_ => {
let position = self.doc.buffer().offset_to_position(offset);
self.proxy.proxy_rpc.get_selection_range(
path.to_owned(),
vec![position],
move |result| {
if let Ok(ProxyResponse::GetSelectionRange { ranges }) =
result
{
let _ = event_sink.submit_command(
LAPCE_UI_COMMAND,
LapceUICommand::StoreSelectionRangeAndApply {
ranges,
rev,
buffer_id,
direction,
current_selection,
},
Target::Auto,
);
}
},
)
}
}
}
}
fn run_motion_mode_command(
&mut self,
_ctx: &mut EventCtx,

View File

@ -25,6 +25,7 @@
pub mod rename;
pub mod rich_text;
pub mod search;
pub mod selection_range;
pub mod settings;
pub mod signature;
pub mod source_control;

View File

@ -0,0 +1,182 @@
use lapce_rpc::buffer::BufferId;
use lsp_types::{Range, SelectionRange};
/// Lsp [selectionRange](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#selectionRange)
/// are used to do "smart" syntax selection. A buffer id, buffer revision and cursor position are
/// stored along side [`lsp_type::SelectionRange`] data to ensure the current selection still apply
/// to the current buffer.
#[derive(Clone, Debug)]
pub struct SyntaxSelectionRanges {
pub buffer_id: BufferId,
pub rev: u64,
pub last_known_selection: Option<(usize, usize)>,
pub ranges: SelectionRange,
pub current_selection: Option<usize>,
}
/// Helper to request either the next or previous [`SyntaxSelectionRanges`],
/// see: [`crate::editor::LapceUiCommand::ApplySelectionRange`]
#[derive(Clone, Copy, Debug)]
pub enum SelectionRangeDirection {
Next,
Previous,
}
impl SyntaxSelectionRanges {
/// Ensure the editor state match this selection range, if not
/// a new SelectionRanges should be requested
pub fn match_request(
&self,
buffer_id: BufferId,
rev: u64,
current_selection: Option<(usize, usize)>,
) -> bool {
if self.last_known_selection.is_some() {
buffer_id == self.buffer_id
&& rev == self.rev
&& current_selection == self.last_known_selection
} else {
buffer_id == self.buffer_id && rev == self.rev
}
}
/// Get the next [`lsp_types::Range'] at `current_selection` depth
pub fn next_range(&mut self) -> Option<Range> {
match self.current_selection {
None => self.current_selection = Some(0),
Some(index) => {
if index < self.count() - 1 {
self.current_selection = Some(index + 1)
}
}
};
self.get()
}
/// Get the previous [`lsp_types::Range'] at `current_selection` depth
pub fn previous_range(&mut self) -> Option<Range> {
if let Some(index) = self.current_selection {
if index > 0 {
self.current_selection = Some(index - 1)
}
}
self.get()
}
fn get(&self) -> Option<Range> {
self.current_selection.and_then(|index| {
if index == 0 {
Some(self.ranges.range)
} else {
let mut current = self.ranges.parent.as_ref();
for _ in 1..index {
current = current.and_then(|c| c.parent.as_ref());
}
current.map(|c| c.range)
}
})
}
fn count(&self) -> usize {
let mut count = 1;
let mut range = &self.ranges;
while let Some(parent) = &range.parent {
count += 1;
range = parent;
}
count
}
}
#[cfg(test)]
mod test {
use crate::command::CommandExecuted::No;
use crate::selection_range::SyntaxSelectionRanges;
use lapce_rpc::buffer::BufferId;
use lsp_types::{Position, Range, SelectionRange};
#[test]
fn should_get_next_selection_range() {
let range_zero = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
};
let range_one = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 2,
},
};
let range_two = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 4,
},
};
let mut syntax_selection = SyntaxSelectionRanges {
buffer_id: BufferId(0),
rev: 0,
last_known_selection: None,
ranges: SelectionRange {
range: range_zero.clone(),
parent: Some(Box::new(SelectionRange {
range: range_one.clone(),
parent: Some(Box::new(SelectionRange {
range: range_two.clone(),
parent: None,
})),
})),
},
current_selection: None,
};
let range = syntax_selection.next_range();
assert_eq!(range, Some(range_zero));
assert_eq!(syntax_selection.current_selection, Some(0));
let range = syntax_selection.next_range();
assert_eq!(range, Some(range_one));
assert_eq!(syntax_selection.current_selection, Some(1));
let range = syntax_selection.next_range();
assert_eq!(range, Some(range_two));
assert_eq!(syntax_selection.current_selection, Some(2));
// Ensure we are not going out of bound
let range = syntax_selection.next_range();
assert_eq!(range, Some(range_two));
assert_eq!(syntax_selection.current_selection, Some(2));
// Going backward now
let range = syntax_selection.previous_range();
assert_eq!(range, Some(range_one));
assert_eq!(syntax_selection.current_selection, Some(1));
let range = syntax_selection.previous_range();
assert_eq!(range, Some(range_zero));
assert_eq!(syntax_selection.current_selection, Some(0));
// Ensure we are not going below zero
let range = syntax_selection.previous_range();
assert_eq!(range, Some(range_zero));
assert_eq!(syntax_selection.current_selection, Some(0));
}
}

View File

@ -708,6 +708,19 @@ fn handle_request(&mut self, id: RequestId, rpc: ProxyRequest) {
};
self.respond_rpc(id, result);
}
GetSelectionRange { positions, path } => {
let proxy_rpc = self.proxy_rpc.clone();
self.catalog_rpc.get_selection_range(
path.as_path(),
positions,
move |_, result| {
let result = result.map(|ranges| {
ProxyResponse::GetSelectionRange { ranges }
});
proxy_rpc.handle_response(id, result);
},
);
}
}
}
}

View File

@ -15,8 +15,8 @@
use lsp_types::request::{
CodeActionRequest, Completion, DocumentSymbolRequest, Formatting,
GotoDefinition, GotoTypeDefinition, GotoTypeDefinitionParams,
GotoTypeDefinitionResponse, HoverRequest, InlayHintRequest,
PrepareRenameRequest, References, Rename, Request, ResolveCompletionItem,
GotoTypeDefinitionResponse, InlayHintRequest, PrepareRenameRequest, References,
Rename, Request, ResolveCompletionItem, SelectionRangeRequest,
SemanticTokensFullRequest, WorkspaceSymbol,
};
use lsp_types::{
@ -24,12 +24,12 @@
CompletionParams, CompletionResponse, DidOpenTextDocumentParams,
DocumentFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
FormattingOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover,
HoverParams, InlayHint, InlayHintParams, Location, PartialResultParams,
Position, PrepareRenameResponse, Range, ReferenceContext, ReferenceParams,
RenameParams, SemanticTokens, SemanticTokensParams, SymbolInformation,
TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, TextEdit,
Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams, WorkspaceEdit,
WorkspaceSymbolParams,
InlayHint, InlayHintParams, Location, PartialResultParams, Position,
PrepareRenameResponse, Range, ReferenceContext, ReferenceParams, RenameParams,
SelectionRange, SelectionRangeParams, SemanticTokens, SemanticTokensParams,
SymbolInformation, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, TextEdit, Url, VersionedTextDocumentIdentifier,
WorkDoneProgressParams, WorkspaceEdit, WorkspaceSymbolParams,
};
use parking_lot::Mutex;
use serde::de::DeserializeOwned;
@ -683,6 +683,34 @@ pub fn get_semantic_tokens(
);
}
pub fn get_selection_range(
&self,
path: &Path,
positions: Vec<Position>,
cb: impl FnOnce(PluginId, Result<Vec<SelectionRange>, RpcError>)
+ Clone
+ Send
+ 'static,
) {
let uri = Url::from_file_path(path).unwrap();
let method = SelectionRangeRequest::METHOD;
let params = SelectionRangeParams {
text_document: TextDocumentIdentifier { uri },
positions,
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: Default::default(),
};
let language_id =
Some(language_id_from_path(path).unwrap_or("").to_string());
self.send_request_to_all_plugins(
method,
params,
language_id,
Some(path.to_path_buf()),
cb,
);
}
pub fn hover(
&self,
path: &Path,
@ -690,16 +718,16 @@ pub fn hover(
cb: impl FnOnce(PluginId, Result<Hover, RpcError>) + Clone + Send + 'static,
) {
let uri = Url::from_file_path(path).unwrap();
let method = HoverRequest::METHOD;
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
let method = SelectionRangeRequest::METHOD;
let params = SelectionRangeParams {
text_document: TextDocumentIdentifier { uri },
positions: vec![position],
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: Default::default(),
};
let language_id =
Some(language_id_from_path(path).unwrap_or("").to_string());
self.send_request_to_all_plugins(
method,
params,

View File

@ -27,8 +27,8 @@
CodeActionRequest, Completion, DocumentSymbolRequest, Formatting,
GotoDefinition, GotoTypeDefinition, HoverRequest, Initialize,
InlayHintRequest, PrepareRenameRequest, References, RegisterCapability,
Rename, ResolveCompletionItem, SemanticTokensFullRequest,
WorkDoneProgressCreate, WorkspaceSymbol,
Rename, ResolveCompletionItem, SelectionRangeRequest,
SemanticTokensFullRequest, WorkDoneProgressCreate, WorkspaceSymbol,
},
CodeActionProviderCapability, DidChangeTextDocumentParams,
DidSaveTextDocumentParams, DocumentSelector, HoverProviderCapability, OneOf,
@ -692,6 +692,9 @@ pub fn method_registered(&mut self, method: &'static str) -> bool {
self.server_capabilities.rename_provider.is_some()
}
Rename::METHOD => self.server_capabilities.rename_provider.is_some(),
SelectionRangeRequest::METHOD => {
self.server_capabilities.selection_range_provider.is_some()
}
_ => false,
}
}

View File

@ -11,8 +11,8 @@
use lsp_types::{
request::GotoTypeDefinitionResponse, CodeActionResponse, CompletionItem,
DocumentSymbolResponse, GotoDefinitionResponse, Hover, InlayHint, Location,
Position, PrepareRenameResponse, SymbolInformation, TextDocumentItem, TextEdit,
WorkspaceEdit,
Position, PrepareRenameResponse, SelectionRange, SymbolInformation,
TextDocumentItem, TextEdit, WorkspaceEdit,
};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
@ -61,6 +61,10 @@ pub enum ProxyRequest {
buffer_id: BufferId,
position: Position,
},
GetSelectionRange {
path: PathBuf,
positions: Vec<Position>,
},
GetReferences {
path: PathBuf,
position: Position,
@ -255,6 +259,9 @@ pub enum ProxyResponse {
GetWorkspaceSymbols {
symbols: Vec<SymbolInformation>,
},
GetSelectionRange {
ranges: Vec<SelectionRange>,
},
GetInlayHints {
hints: Vec<InlayHint>,
},
@ -732,6 +739,15 @@ pub fn git_discard_files_changes(&self, files: Vec<PathBuf>) {
pub fn git_discard_workspace_changes(&self) {
self.notification(ProxyNotification::GitDiscardWorkspaceChanges {});
}
pub fn get_selection_range(
&self,
path: PathBuf,
positions: Vec<Position>,
f: impl ProxyCallback + 'static,
) {
self.request_async(ProxyRequest::GetSelectionRange { path, positions }, f);
}
}
impl Default for ProxyRpcHandler {

View File

@ -26,6 +26,7 @@
use lapce_data::menu::MenuKind;
use lapce_data::palette::PaletteStatus;
use lapce_data::panel::{PanelData, PanelKind};
use lapce_data::selection_range::SyntaxSelectionRanges;
use lapce_data::{
command::{
LapceCommand, LapceUICommand, LapceWorkbenchCommand, LAPCE_UI_COMMAND,
@ -1911,62 +1912,159 @@ fn event(
}
}
Event::Command(cmd) if cmd.is(LAPCE_UI_COMMAND) => {
let cmd = cmd.get_unchecked(LAPCE_UI_COMMAND);
if let LapceUICommand::ShowCodeActions(point) = cmd {
let editor_data = data.editor_view_content(self.view_id);
if let Some(actions) = editor_data.current_code_actions() {
if !actions.is_empty() {
let mut menu = druid::Menu::new("");
match cmd.get_unchecked(LAPCE_UI_COMMAND) {
LapceUICommand::ShowCodeActions(point) => {
let editor_data = data.editor_view_content(self.view_id);
if let Some(actions) = editor_data.current_code_actions() {
if !actions.is_empty() {
let mut menu = druid::Menu::new("");
for action in actions.iter() {
let title = match action {
CodeActionOrCommand::Command(c) => {
c.title.clone()
}
CodeActionOrCommand::CodeAction(a) => {
a.title.clone()
}
};
let mut item = druid::MenuItem::new(title);
item = item.command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::RunCodeAction(action.clone()),
Target::Widget(editor_data.view_id),
));
menu = menu.entry(item);
for action in actions.iter() {
let title = match action {
CodeActionOrCommand::Command(c) => {
c.title.clone()
}
CodeActionOrCommand::CodeAction(a) => {
a.title.clone()
}
};
let mut item = druid::MenuItem::new(title);
item = item.command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::RunCodeAction(
action.clone(),
),
Target::Widget(editor_data.view_id),
));
menu = menu.entry(item);
}
let point = point.unwrap_or_else(|| {
let offset = editor_data.editor.cursor.offset();
let (line, col) = editor_data
.doc
.buffer()
.offset_to_line_col(offset);
let phantom_text = editor_data
.doc
.line_phantom_text(&data.config, line);
let col = phantom_text.col_at(col);
let x = editor_data
.doc
.line_point_of_line_col(
ctx.text(),
line,
col,
editor_data.config.editor.font_size,
&editor_data.config,
)
.x;
let y = editor_data.config.editor.line_height()
as f64
* (line + 1) as f64;
ctx.to_window(Point::new(x, y))
});
ctx.show_context_menu::<LapceData>(menu, point);
}
let point = point.unwrap_or_else(|| {
let offset = editor_data.editor.cursor.offset();
let (line, col) = editor_data
.doc
.buffer()
.offset_to_line_col(offset);
let phantom_text = editor_data
.doc
.line_phantom_text(&data.config, line);
let col = phantom_text.col_at(col);
let x = editor_data
.doc
.line_point_of_line_col(
ctx.text(),
line,
col,
editor_data.config.editor.font_size,
&editor_data.config,
)
.x;
let y = editor_data.config.editor.line_height()
as f64
* (line + 1) as f64;
ctx.to_window(Point::new(x, y))
});
ctx.show_context_menu::<LapceData>(menu, point);
}
}
LapceUICommand::ApplySelectionRange {
buffer_id,
rev,
direction,
} => {
if let Some(editor) = data
.main_split
.active
.and_then(|active| data.main_split.editors.get(&active))
.cloned()
{
let mut editor_data =
data.editor_view_content(editor.view_id);
let orig_doc =
data.main_split.editor_doc(editor.view_id);
if orig_doc.id() != *buffer_id || orig_doc.rev() != *rev
{
return;
}
let doc = Arc::make_mut(&mut editor_data.doc);
if let Some(selection) =
doc.change_syntax_selection(*direction)
{
Arc::make_mut(&mut editor_data.editor)
.cursor
.update_selection(orig_doc.buffer(), selection);
data.update_from_editor_buffer_data(
editor_data,
&editor,
&orig_doc,
);
}
}
}
LapceUICommand::StoreSelectionRangeAndApply {
rev,
buffer_id,
current_selection,
ranges,
direction,
} => {
if let Some(editor) = data
.main_split
.active
.and_then(|active| data.main_split.editors.get(&active))
.cloned()
{
let mut editor_data =
data.editor_view_content(editor.view_id);
let orig_doc =
data.main_split.editor_doc(editor.view_id);
if orig_doc.id() != *buffer_id || orig_doc.rev() != *rev
{
return;
}
let mut doc = Arc::make_mut(&mut editor_data.doc);
if let (_, Some(ranges)) = (
&doc.syntax_selection_range,
ranges.first().cloned(),
) {
doc.syntax_selection_range =
Some(SyntaxSelectionRanges {
buffer_id: *buffer_id,
rev: *rev,
last_known_selection: *current_selection,
ranges,
current_selection: None,
});
};
data.update_from_editor_buffer_data(
editor_data,
&editor,
&orig_doc,
);
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::ApplySelectionRange {
buffer_id: *buffer_id,
rev: *rev,
direction: *direction,
},
Target::Auto,
));
}
}
_ => {}
}
}
_ => (),