use crate::{
assist_config::{AssistConfig, SnippetCap},
- Assist, AssistId, AssistKind, GroupLabel, ResolvedAssist,
+ Assist, AssistId, AssistKind, GroupLabel,
};
/// `AssistContext` allows to apply an assist or check if it could be applied.
pub(crate) struct Assists {
resolve: bool,
file: FileId,
- buf: Vec<(Assist, Option<SourceChange>)>,
+ buf: Vec<Assist>,
allowed: Option<Vec<AssistKind>>,
}
impl Assists {
- pub(crate) fn new_resolved(ctx: &AssistContext) -> Assists {
+ pub(crate) fn new(ctx: &AssistContext, resolve: bool) -> Assists {
Assists {
- resolve: true,
+ resolve,
file: ctx.frange.file_id,
buf: Vec::new(),
allowed: ctx.config.allowed.clone(),
}
}
- pub(crate) fn new_unresolved(ctx: &AssistContext) -> Assists {
- Assists {
- resolve: false,
- file: ctx.frange.file_id,
- buf: Vec::new(),
- allowed: ctx.config.allowed.clone(),
- }
- }
-
- pub(crate) fn finish_unresolved(self) -> Vec<Assist> {
- assert!(!self.resolve);
- self.finish()
- .into_iter()
- .map(|(label, edit)| {
- assert!(edit.is_none());
- label
- })
- .collect()
- }
-
- pub(crate) fn finish_resolved(self) -> Vec<ResolvedAssist> {
- assert!(self.resolve);
- self.finish()
- .into_iter()
- .map(|(label, edit)| ResolvedAssist { assist: label, source_change: edit.unwrap() })
- .collect()
+ pub(crate) fn finish(mut self) -> Vec<Assist> {
+ self.buf.sort_by_key(|assist| assist.target.len());
+ self.buf
}
pub(crate) fn add(
return None;
}
let label = Label::new(label.into());
- let assist = Assist { id, label, group: None, target };
+ let assist = Assist { id, label, group: None, target, source_change: None };
self.add_impl(assist, f)
}
return None;
}
let label = Label::new(label.into());
- let assist = Assist { id, label, group: Some(group.clone()), target };
+ let assist = Assist { id, label, group: Some(group.clone()), target, source_change: None };
self.add_impl(assist, f)
}
- fn add_impl(&mut self, assist: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> {
+ fn add_impl(&mut self, mut assist: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> {
let source_change = if self.resolve {
let mut builder = AssistBuilder::new(self.file);
f(&mut builder);
} else {
None
};
+ assist.source_change = source_change.clone();
- self.buf.push((assist, source_change));
+ self.buf.push(assist);
Some(())
}
- fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> {
- self.buf.sort_by_key(|(label, _edit)| label.target.len());
- self.buf
- }
-
fn is_allowed(&self, id: &AssistId) -> bool {
match &self.allowed {
Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)),
/// Target ranges are used to sort assists: the smaller the target range,
/// the more specific assist is, and so it should be sorted first.
pub target: TextRange,
-}
-
-#[derive(Debug, Clone)]
-pub struct ResolvedAssist {
- pub assist: Assist,
- pub source_change: SourceChange,
+ /// Computing source change sometimes is much more costly then computing the
+ /// other fields. Additionally, the actual change is not required to show
+ /// the lightbulb UI, it only is needed when the user tries to apply an
+ /// assist. So, we compute it lazily: the API allow requesting assists with
+ /// or without source change. We could (and in fact, used to) distinguish
+ /// between resolved and unresolved assists at the type level, but this is
+ /// cumbersome, especially if you want to embed an assist into another data
+ /// structure, such as a diagnostic.
+ pub source_change: Option<SourceChange>,
}
impl Assist {
/// Return all the assists applicable at the given position.
- ///
- /// Assists are returned in the "unresolved" state, that is only labels are
- /// returned, without actual edits.
- pub fn unresolved(db: &RootDatabase, config: &AssistConfig, range: FileRange) -> Vec<Assist> {
- let sema = Semantics::new(db);
- let ctx = AssistContext::new(sema, config, range);
- let mut acc = Assists::new_unresolved(&ctx);
- handlers::all().iter().for_each(|handler| {
- handler(&mut acc, &ctx);
- });
- acc.finish_unresolved()
- }
-
- /// Return all the assists applicable at the given position.
- ///
- /// Assists are returned in the "resolved" state, that is with edit fully
- /// computed.
- pub fn resolved(
+ pub fn get(
db: &RootDatabase,
config: &AssistConfig,
+ resolve: bool,
range: FileRange,
- ) -> Vec<ResolvedAssist> {
+ ) -> Vec<Assist> {
let sema = Semantics::new(db);
let ctx = AssistContext::new(sema, config, range);
- let mut acc = Assists::new_resolved(&ctx);
+ let mut acc = Assists::new(&ctx, resolve);
handlers::all().iter().for_each(|handler| {
handler(&mut acc, &ctx);
});
- acc.finish_resolved()
+ acc.finish()
}
}
let before = db.file_text(file_id).to_string();
let frange = FileRange { file_id, range: selection.into() };
- let assist = Assist::resolved(&db, &AssistConfig::default(), frange)
+ let assist = Assist::get(&db, &AssistConfig::default(), true, frange)
.into_iter()
- .find(|assist| assist.assist.id.0 == assist_id)
+ .find(|assist| assist.id.0 == assist_id)
.unwrap_or_else(|| {
panic!(
"\n\nAssist is not applicable: {}\nAvailable assists: {}",
assist_id,
- Assist::resolved(&db, &AssistConfig::default(), frange)
+ Assist::get(&db, &AssistConfig::default(), false, frange)
.into_iter()
- .map(|assist| assist.assist.id.0)
+ .map(|assist| assist.id.0)
.collect::<Vec<_>>()
.join(", ")
)
});
let actual = {
+ let source_change = assist.source_change.unwrap();
let mut actual = before;
- for source_file_edit in assist.source_change.source_file_edits {
+ for source_file_edit in source_change.source_file_edits {
if source_file_edit.file_id == file_id {
source_file_edit.edit.apply(&mut actual)
}
let sema = Semantics::new(&db);
let config = AssistConfig::default();
let ctx = AssistContext::new(sema, &config, frange);
- let mut acc = Assists::new_resolved(&ctx);
+ let mut acc = Assists::new(&ctx, true);
handler(&mut acc, &ctx);
- let mut res = acc.finish_resolved();
+ let mut res = acc.finish();
let assist = match assist_label {
- Some(label) => res.into_iter().find(|resolved| resolved.assist.label == label),
+ Some(label) => res.into_iter().find(|resolved| resolved.label == label),
None => res.pop(),
};
match (assist, expected) {
(Some(assist), ExpectedResult::After(after)) => {
- let mut source_change = assist.source_change;
+ let mut source_change = assist.source_change.unwrap();
assert!(!source_change.source_file_edits.is_empty());
let skip_header = source_change.source_file_edits.len() == 1
&& source_change.file_system_edits.len() == 0;
assert_eq_text!(after, &buf);
}
(Some(assist), ExpectedResult::Target(target)) => {
- let range = assist.assist.target;
+ let range = assist.target;
assert_eq_text!(&text_without_caret[range], target);
}
(Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"),
let (before_cursor_pos, before) = extract_offset(before);
let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
- let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
+ let assists = Assist::get(&db, &AssistConfig::default(), false, frange);
let mut assists = assists.iter();
- assert_eq!(
- assists.next().expect("expected assist").assist.label,
- "Change visibility to pub(crate)"
- );
- assert_eq!(assists.next().expect("expected assist").assist.label, "Add `#[derive]`");
+ assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)");
+ assert_eq!(assists.next().expect("expected assist").label, "Add `#[derive]`");
}
#[test]
let (range, before) = extract_range(before);
let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range };
- let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
+ let assists = Assist::get(&db, &AssistConfig::default(), false, frange);
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, "Replace with match");
+ assert_eq!(assists.next().expect("expected assist").label, "Extract into variable");
+ assert_eq!(assists.next().expect("expected assist").label, "Replace with match");
}
#[test]
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::Refactor]);
- let assists = Assist::resolved(&db, &cfg, frange);
+ let assists = Assist::get(&db, &cfg, false, frange);
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, "Replace with match");
+ assert_eq!(assists.next().expect("expected assist").label, "Extract into variable");
+ assert_eq!(assists.next().expect("expected assist").label, "Replace with match");
}
{
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
- let assists = Assist::resolved(&db, &cfg, frange);
+ let assists = Assist::get(&db, &cfg, false, frange);
assert_eq!(assists.len(), 1);
let mut assists = assists.iter();
- assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");
+ assert_eq!(assists.next().expect("expected assist").label, "Extract into variable");
}
{
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::QuickFix]);
- let assists = Assist::resolved(&db, &cfg, frange);
+ let assists = Assist::get(&db, &cfg, false, frange);
assert!(assists.is_empty(), "All asserts but quickfixes should be filtered out");
}
}
HighlightedRange,
},
};
-pub use assists::{Assist, AssistConfig, AssistId, AssistKind, ResolvedAssist};
+pub use assists::{Assist, AssistConfig, AssistId, AssistKind};
pub use completion::{
CompletionConfig, CompletionItem, CompletionItemKind, CompletionResolveCapability,
CompletionScore, ImportEdit, InsertTextFormat,
}
/// Computes assists (aka code actions aka intentions) for the given
- /// position. Computes enough info to show the lightbulb list in the editor,
- /// but doesn't compute actual edits, to improve performance.
- ///
- /// When the user clicks on the assist, call `resolve_assists` to get the
- /// edit.
- pub fn assists(&self, config: &AssistConfig, frange: FileRange) -> Cancelable<Vec<Assist>> {
- self.with_db(|db| Assist::unresolved(db, config, frange))
- }
-
- /// Computes resolved assists with source changes for the given position.
- pub fn resolve_assists(
+ /// position. If `resolve == false`, computes enough info to show the
+ /// lightbulb list in the editor, but doesn't compute actual edits, to
+ /// improve performance.
+ pub fn assists(
&self,
config: &AssistConfig,
+ resolve: bool,
frange: FileRange,
- ) -> Cancelable<Vec<ResolvedAssist>> {
- self.with_db(|db| assists::Assist::resolved(db, config, frange))
+ ) -> Cancelable<Vec<Assist>> {
+ self.with_db(|db| Assist::get(db, config, resolve, frange))
}
/// Computes the set of diagnostics for the given file.
if snap.config.client_caps.code_action_resolve {
for (index, assist) in
- snap.analysis.assists(&assists_config, frange)?.into_iter().enumerate()
+ snap.analysis.assists(&assists_config, false, frange)?.into_iter().enumerate()
{
res.push(to_proto::unresolved_code_action(&snap, params.clone(), assist, index)?);
}
} else {
- for assist in snap.analysis.resolve_assists(&assists_config, frange)?.into_iter() {
+ for assist in snap.analysis.assists(&assists_config, true, frange)?.into_iter() {
res.push(to_proto::resolved_code_action(&snap, assist)?);
}
}
.only
.map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect());
- let assists = snap.analysis.resolve_assists(&snap.config.assist, frange)?;
+ let assists = snap.analysis.assists(&snap.config.assist, true, frange)?;
let (id, index) = split_once(¶ms.id, ':').unwrap();
let index = index.parse::<usize>().unwrap();
let assist = &assists[index];
- assert!(assist.assist.id.0 == id);
+ assert!(assist.id.0 == id);
let edit = to_proto::resolved_code_action(&snap, assist.clone())?.edit;
code_action.edit = edit;
Ok(code_action)
Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, Documentation, FileId,
FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HighlightModifier, HighlightTag,
HighlightedRange, Indel, InlayHint, InlayKind, InsertTextFormat, LineIndex, Markup,
- NavigationTarget, ReferenceAccess, ResolvedAssist, Runnable, Severity, SourceChange,
- SourceFileEdit, SymbolKind, TextEdit, TextRange, TextSize,
+ NavigationTarget, ReferenceAccess, Runnable, Severity, SourceChange, SourceFileEdit,
+ SymbolKind, TextEdit, TextRange, TextSize,
};
use itertools::Itertools;
assist: Assist,
index: usize,
) -> Result<lsp_ext::CodeAction> {
+ assert!(assist.source_change.is_none());
let res = lsp_ext::CodeAction {
title: assist.label.to_string(),
group: assist.group.filter(|_| snap.config.client_caps.code_action_group).map(|gr| gr.0),
pub(crate) fn resolved_code_action(
snap: &GlobalStateSnapshot,
- assist: ResolvedAssist,
+ assist: Assist,
) -> Result<lsp_ext::CodeAction> {
- let change = assist.source_change;
+ let change = assist.source_change.unwrap();
let res = lsp_ext::CodeAction {
edit: Some(snippet_workspace_edit(snap, change)?),
- title: assist.assist.label.to_string(),
- group: assist
- .assist
- .group
- .filter(|_| snap.config.client_caps.code_action_group)
- .map(|gr| gr.0),
- kind: Some(code_action_kind(assist.assist.id.1)),
+ title: assist.label.to_string(),
+ group: assist.group.filter(|_| snap.config.client_caps.code_action_group).map(|gr| gr.0),
+ kind: Some(code_action_kind(assist.id.1)),
is_preferred: None,
data: None,
};