Add AssistConfig

This commit is contained in:
Aleksey Kladov 2020-05-17 12:09:53 +02:00
parent 131849f2ab
commit c847c079fd
13 changed files with 129 additions and 39 deletions

View file

@ -0,0 +1,27 @@
//! Settings for tweaking assists.
//!
//! The fun thing here is `SnippetCap` -- this type can only be created in this
//! module, and we use to statically check that we only produce snippet
//! assists if we are allowed to.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssistConfig {
pub snippet_cap: Option<SnippetCap>,
}
impl AssistConfig {
pub fn allow_snippets(&mut self, yes: bool) {
self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SnippetCap {
_private: (),
}
impl Default for AssistConfig {
fn default() -> Self {
AssistConfig { snippet_cap: Some(SnippetCap { _private: () }) }
}
}

View file

@ -15,7 +15,10 @@ use ra_syntax::{
}; };
use ra_text_edit::TextEditBuilder; use ra_text_edit::TextEditBuilder;
use crate::{Assist, AssistId, GroupLabel, ResolvedAssist}; use crate::{
assist_config::{AssistConfig, SnippetCap},
Assist, AssistId, GroupLabel, ResolvedAssist,
};
/// `AssistContext` allows to apply an assist or check if it could be applied. /// `AssistContext` allows to apply an assist or check if it could be applied.
/// ///
@ -48,6 +51,7 @@ use crate::{Assist, AssistId, GroupLabel, ResolvedAssist};
/// moment, because the LSP API is pretty awkward in this place, and it's much /// moment, because the LSP API is pretty awkward in this place, and it's much
/// easier to just compute the edit eagerly :-) /// easier to just compute the edit eagerly :-)
pub(crate) struct AssistContext<'a> { pub(crate) struct AssistContext<'a> {
pub(crate) config: &'a AssistConfig,
pub(crate) sema: Semantics<'a, RootDatabase>, pub(crate) sema: Semantics<'a, RootDatabase>,
pub(crate) db: &'a RootDatabase, pub(crate) db: &'a RootDatabase,
pub(crate) frange: FileRange, pub(crate) frange: FileRange,
@ -55,10 +59,14 @@ pub(crate) struct AssistContext<'a> {
} }
impl<'a> AssistContext<'a> { impl<'a> AssistContext<'a> {
pub fn new(sema: Semantics<'a, RootDatabase>, frange: FileRange) -> AssistContext<'a> { pub(crate) fn new(
sema: Semantics<'a, RootDatabase>,
config: &'a AssistConfig,
frange: FileRange,
) -> AssistContext<'a> {
let source_file = sema.parse(frange.file_id); let source_file = sema.parse(frange.file_id);
let db = sema.db; let db = sema.db;
AssistContext { sema, db, frange, source_file } AssistContext { config, sema, db, frange, source_file }
} }
// NB, this ignores active selection. // NB, this ignores active selection.
@ -165,11 +173,17 @@ pub(crate) struct AssistBuilder {
edit: TextEditBuilder, edit: TextEditBuilder,
cursor_position: Option<TextSize>, cursor_position: Option<TextSize>,
file: FileId, file: FileId,
is_snippet: bool,
} }
impl AssistBuilder { impl AssistBuilder {
pub(crate) fn new(file: FileId) -> AssistBuilder { pub(crate) fn new(file: FileId) -> AssistBuilder {
AssistBuilder { edit: TextEditBuilder::default(), cursor_position: None, file } AssistBuilder {
edit: TextEditBuilder::default(),
cursor_position: None,
file,
is_snippet: false,
}
} }
/// Remove specified `range` of text. /// Remove specified `range` of text.
@ -180,10 +194,30 @@ impl AssistBuilder {
pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) { pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) {
self.edit.insert(offset, text.into()) self.edit.insert(offset, text.into())
} }
/// Append specified `text` at the given `offset`
pub(crate) fn insert_snippet(
&mut self,
_cap: SnippetCap,
offset: TextSize,
text: impl Into<String>,
) {
self.is_snippet = true;
self.edit.insert(offset, text.into())
}
/// Replaces specified `range` of text with a given string. /// Replaces specified `range` of text with a given string.
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
self.edit.replace(range, replace_with.into()) self.edit.replace(range, replace_with.into())
} }
/// Append specified `text` at the given `offset`
pub(crate) fn replace_snippet(
&mut self,
_cap: SnippetCap,
range: TextRange,
replace_with: impl Into<String>,
) {
self.is_snippet = true;
self.edit.replace(range, replace_with.into())
}
pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) { pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit)
} }
@ -227,7 +261,12 @@ impl AssistBuilder {
if edit.is_empty() && self.cursor_position.is_none() { if edit.is_empty() && self.cursor_position.is_none() {
panic!("Only call `add_assist` if the assist can be applied") panic!("Only call `add_assist` if the assist can be applied")
} }
SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } let mut res =
.into_source_change(self.file) SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position }
.into_source_change(self.file);
if self.is_snippet {
res.is_snippet = true;
}
res
} }
} }

View file

@ -10,6 +10,7 @@ macro_rules! eprintln {
($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; ($($tt:tt)*) => { stdx::eprintln!($($tt)*) };
} }
mod assist_config;
mod assist_context; mod assist_context;
mod marks; mod marks;
#[cfg(test)] #[cfg(test)]
@ -24,6 +25,8 @@ use ra_syntax::TextRange;
pub(crate) use crate::assist_context::{AssistContext, Assists}; pub(crate) use crate::assist_context::{AssistContext, Assists};
pub use assist_config::AssistConfig;
/// Unique identifier of the assist, should not be shown to the user /// Unique identifier of the assist, should not be shown to the user
/// directly. /// directly.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -54,9 +57,9 @@ impl Assist {
/// ///
/// Assists are returned in the "unresolved" state, that is only labels are /// Assists are returned in the "unresolved" state, that is only labels are
/// returned, without actual edits. /// returned, without actual edits.
pub fn unresolved(db: &RootDatabase, range: FileRange) -> Vec<Assist> { pub fn unresolved(db: &RootDatabase, config: &AssistConfig, range: FileRange) -> Vec<Assist> {
let sema = Semantics::new(db); let sema = Semantics::new(db);
let ctx = AssistContext::new(sema, range); let ctx = AssistContext::new(sema, config, range);
let mut acc = Assists::new_unresolved(&ctx); let mut acc = Assists::new_unresolved(&ctx);
handlers::all().iter().for_each(|handler| { handlers::all().iter().for_each(|handler| {
handler(&mut acc, &ctx); handler(&mut acc, &ctx);
@ -68,9 +71,13 @@ impl Assist {
/// ///
/// Assists are returned in the "resolved" state, that is with edit fully /// Assists are returned in the "resolved" state, that is with edit fully
/// computed. /// computed.
pub fn resolved(db: &RootDatabase, range: FileRange) -> Vec<ResolvedAssist> { pub fn resolved(
db: &RootDatabase,
config: &AssistConfig,
range: FileRange,
) -> Vec<ResolvedAssist> {
let sema = Semantics::new(db); let sema = Semantics::new(db);
let ctx = AssistContext::new(sema, range); let ctx = AssistContext::new(sema, config, range);
let mut acc = Assists::new_resolved(&ctx); let mut acc = Assists::new_resolved(&ctx);
handlers::all().iter().for_each(|handler| { handlers::all().iter().for_each(|handler| {
handler(&mut acc, &ctx); handler(&mut acc, &ctx);

View file

@ -11,7 +11,7 @@ use test_utils::{
RangeOrOffset, RangeOrOffset,
}; };
use crate::{handlers::Handler, Assist, AssistContext, Assists}; use crate::{handlers::Handler, Assist, AssistConfig, AssistContext, Assists};
pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
let (mut db, file_id) = RootDatabase::with_single_file(text); let (mut db, file_id) = RootDatabase::with_single_file(text);
@ -41,14 +41,14 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
let (db, file_id) = crate::tests::with_single_file(&before); let (db, file_id) = crate::tests::with_single_file(&before);
let frange = FileRange { file_id, range: selection.into() }; let frange = FileRange { file_id, range: selection.into() };
let mut assist = Assist::resolved(&db, frange) let mut assist = Assist::resolved(&db, &AssistConfig::default(), frange)
.into_iter() .into_iter()
.find(|assist| assist.assist.id.0 == assist_id) .find(|assist| assist.assist.id.0 == assist_id)
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!( panic!(
"\n\nAssist is not applicable: {}\nAvailable assists: {}", "\n\nAssist is not applicable: {}\nAvailable assists: {}",
assist_id, assist_id,
Assist::resolved(&db, frange) Assist::resolved(&db, &AssistConfig::default(), frange)
.into_iter() .into_iter()
.map(|assist| assist.assist.id.0) .map(|assist| assist.assist.id.0)
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -90,7 +90,8 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult) {
let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() }; let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() };
let sema = Semantics::new(&db); let sema = Semantics::new(&db);
let ctx = AssistContext::new(sema, frange); let config = AssistConfig::default();
let ctx = AssistContext::new(sema, &config, frange);
let mut acc = Assists::new_resolved(&ctx); let mut acc = Assists::new_resolved(&ctx);
handler(&mut acc, &ctx); handler(&mut acc, &ctx);
let mut res = acc.finish_resolved(); let mut res = acc.finish_resolved();
@ -103,19 +104,20 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult) {
let mut actual = db.file_text(change.file_id).as_ref().to_owned(); let mut actual = db.file_text(change.file_id).as_ref().to_owned();
change.edit.apply(&mut actual); change.edit.apply(&mut actual);
match source_change.cursor_position { if !source_change.is_snippet {
None => { match source_change.cursor_position {
if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset { None => {
let off = change if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset {
.edit let off = change
.apply_to_offset(before_cursor_pos) .edit
.expect("cursor position is affected by the edit"); .apply_to_offset(before_cursor_pos)
actual = add_cursor(&actual, off) .expect("cursor position is affected by the edit");
actual = add_cursor(&actual, off)
}
} }
} Some(off) => actual = add_cursor(&actual, off.offset),
Some(off) => actual = add_cursor(&actual, off.offset), };
}; }
assert_eq_text!(after, &actual); assert_eq_text!(after, &actual);
} }
(Some(assist), ExpectedResult::Target(target)) => { (Some(assist), ExpectedResult::Target(target)) => {
@ -136,7 +138,7 @@ fn assist_order_field_struct() {
let (before_cursor_pos, before) = extract_offset(before); let (before_cursor_pos, before) = extract_offset(before);
let (db, file_id) = with_single_file(&before); let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) }; let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
let assists = Assist::resolved(&db, frange); let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
let mut assists = assists.iter(); let mut assists = assists.iter();
assert_eq!( assert_eq!(
@ -159,7 +161,7 @@ fn assist_order_if_expr() {
let (range, before) = extract_range(before); let (range, before) = extract_range(before);
let (db, file_id) = with_single_file(&before); let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range }; let frange = FileRange { file_id, range };
let assists = Assist::resolved(&db, frange); let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
let mut assists = assists.iter(); let mut assists = assists.iter();
assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable"); assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");

View file

@ -59,8 +59,8 @@ pub use crate::completion::{
/// with ordering of completions (currently this is done by the client). /// with ordering of completions (currently this is done by the client).
pub(crate) fn completions( pub(crate) fn completions(
db: &RootDatabase, db: &RootDatabase,
position: FilePosition,
config: &CompletionConfig, config: &CompletionConfig,
position: FilePosition,
) -> Option<Completions> { ) -> Option<Completions> {
let ctx = CompletionContext::new(db, position, config)?; let ctx = CompletionContext::new(db, position, config)?;

View file

@ -20,7 +20,7 @@ pub(crate) fn do_completion_with_options(
} else { } else {
single_file_with_position(code) single_file_with_position(code)
}; };
let completions = analysis.completions(position, options).unwrap().unwrap(); let completions = analysis.completions(options, position).unwrap().unwrap();
let completion_items: Vec<CompletionItem> = completions.into(); let completion_items: Vec<CompletionItem> = completions.into();
let mut kind_completions: Vec<CompletionItem> = let mut kind_completions: Vec<CompletionItem> =
completion_items.into_iter().filter(|c| c.completion_kind == kind).collect(); completion_items.into_iter().filter(|c| c.completion_kind == kind).collect();

View file

@ -629,6 +629,7 @@ mod tests {
}, },
], ],
cursor_position: None, cursor_position: None,
is_snippet: false,
}, },
), ),
severity: Error, severity: Error,
@ -685,6 +686,7 @@ mod tests {
], ],
file_system_edits: [], file_system_edits: [],
cursor_position: None, cursor_position: None,
is_snippet: false,
}, },
), ),
severity: Error, severity: Error,

View file

@ -82,7 +82,7 @@ pub use crate::{
}; };
pub use hir::Documentation; pub use hir::Documentation;
pub use ra_assists::AssistId; pub use ra_assists::{AssistConfig, AssistId};
pub use ra_db::{ pub use ra_db::{
Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId, Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId,
}; };
@ -458,17 +458,17 @@ impl Analysis {
/// Computes completions at the given position. /// Computes completions at the given position.
pub fn completions( pub fn completions(
&self, &self,
position: FilePosition,
config: &CompletionConfig, config: &CompletionConfig,
position: FilePosition,
) -> Cancelable<Option<Vec<CompletionItem>>> { ) -> Cancelable<Option<Vec<CompletionItem>>> {
self.with_db(|db| completion::completions(db, position, config).map(Into::into)) self.with_db(|db| completion::completions(db, config, position).map(Into::into))
} }
/// Computes assists (aka code actions aka intentions) for the given /// Computes assists (aka code actions aka intentions) for the given
/// position. /// position.
pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<Assist>> { pub fn assists(&self, config: &AssistConfig, frange: FileRange) -> Cancelable<Vec<Assist>> {
self.with_db(|db| { self.with_db(|db| {
ra_assists::Assist::resolved(db, frange) ra_assists::Assist::resolved(db, config, frange)
.into_iter() .into_iter()
.map(|assist| Assist { .map(|assist| Assist {
id: assist.assist.id, id: assist.assist.id,

View file

@ -670,6 +670,7 @@ mod tests {
}, },
], ],
cursor_position: None, cursor_position: None,
is_snippet: false,
}, },
}, },
) )
@ -722,6 +723,7 @@ mod tests {
}, },
], ],
cursor_position: None, cursor_position: None,
is_snippet: false,
}, },
}, },
) )
@ -818,6 +820,7 @@ mod tests {
}, },
], ],
cursor_position: None, cursor_position: None,
is_snippet: false,
}, },
}, },
) )

View file

@ -13,6 +13,7 @@ pub struct SourceChange {
pub source_file_edits: Vec<SourceFileEdit>, pub source_file_edits: Vec<SourceFileEdit>,
pub file_system_edits: Vec<FileSystemEdit>, pub file_system_edits: Vec<FileSystemEdit>,
pub cursor_position: Option<FilePosition>, pub cursor_position: Option<FilePosition>,
pub is_snippet: bool,
} }
impl SourceChange { impl SourceChange {
@ -28,6 +29,7 @@ impl SourceChange {
source_file_edits, source_file_edits,
file_system_edits, file_system_edits,
cursor_position: None, cursor_position: None,
is_snippet: false,
} }
} }
@ -41,6 +43,7 @@ impl SourceChange {
source_file_edits: edits, source_file_edits: edits,
file_system_edits: vec![], file_system_edits: vec![],
cursor_position: None, cursor_position: None,
is_snippet: false,
} }
} }
@ -52,6 +55,7 @@ impl SourceChange {
source_file_edits: vec![], source_file_edits: vec![],
file_system_edits: edits, file_system_edits: edits,
cursor_position: None, cursor_position: None,
is_snippet: false,
} }
} }
@ -115,6 +119,7 @@ impl SingleFileChange {
source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }], source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }],
file_system_edits: Vec::new(), file_system_edits: Vec::new(),
cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }), cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }),
is_snippet: false,
} }
} }
} }

View file

@ -105,7 +105,7 @@ pub fn analysis_bench(
if is_completion { if is_completion {
let options = CompletionConfig::default(); let options = CompletionConfig::default();
let res = do_work(&mut host, file_id, |analysis| { let res = do_work(&mut host, file_id, |analysis| {
analysis.completions(file_position, &options) analysis.completions(&options, file_position)
}); });
if verbosity.is_verbose() { if verbosity.is_verbose() {
println!("\n{:#?}", res); println!("\n{:#?}", res);

View file

@ -11,7 +11,7 @@ use std::{ffi::OsString, path::PathBuf};
use lsp_types::ClientCapabilities; use lsp_types::ClientCapabilities;
use ra_flycheck::FlycheckConfig; use ra_flycheck::FlycheckConfig;
use ra_ide::{CompletionConfig, InlayHintsConfig}; use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig};
use ra_project_model::CargoConfig; use ra_project_model::CargoConfig;
use serde::Deserialize; use serde::Deserialize;
@ -32,6 +32,7 @@ pub struct Config {
pub inlay_hints: InlayHintsConfig, pub inlay_hints: InlayHintsConfig,
pub completion: CompletionConfig, pub completion: CompletionConfig,
pub assist: AssistConfig,
pub call_info_full: bool, pub call_info_full: bool,
pub lens: LensConfig, pub lens: LensConfig,
} }
@ -136,6 +137,7 @@ impl Default for Config {
add_call_argument_snippets: true, add_call_argument_snippets: true,
..CompletionConfig::default() ..CompletionConfig::default()
}, },
assist: AssistConfig::default(),
call_info_full: true, call_info_full: true,
lens: LensConfig::default(), lens: LensConfig::default(),
} }
@ -281,6 +283,7 @@ impl Config {
} }
} }
} }
self.assist.allow_snippets(false);
} }
if let Some(window_caps) = caps.window.as_ref() { if let Some(window_caps) = caps.window.as_ref() {

View file

@ -476,7 +476,7 @@ pub fn handle_completion(
return Ok(None); return Ok(None);
} }
let items = match world.analysis().completions(position, &world.config.completion)? { let items = match world.analysis().completions(&world.config.completion, position)? {
None => return Ok(None), None => return Ok(None),
Some(items) => items, Some(items) => items,
}; };
@ -740,7 +740,9 @@ pub fn handle_code_action(
} }
let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default(); let mut grouped_assists: FxHashMap<String, (usize, Vec<Assist>)> = FxHashMap::default();
for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() { for assist in
world.analysis().assists(&world.config.assist, FileRange { file_id, range })?.into_iter()
{
match &assist.group_label { match &assist.group_label {
Some(label) => grouped_assists Some(label) => grouped_assists
.entry(label.to_owned()) .entry(label.to_owned())