1 //! See [`AssistContext`]. 2 3 use hir::Semantics; 4 use ide_db::base_db::{FileId, FileRange}; 5 use ide_db::{label::Label, RootDatabase}; 6 use syntax::{ 7 algo::{self, find_node_at_offset, find_node_at_range}, 8 AstNode, AstToken, Direction, SourceFile, SyntaxElement, SyntaxKind, SyntaxToken, TextRange, 9 TextSize, TokenAtOffset, 10 }; 11 12 use crate::{ 13 assist_config::AssistConfig, Assist, AssistId, AssistKind, AssistResolveStrategy, GroupLabel, 14 }; 15 16 pub(crate) use ide_db::source_change::{SourceChangeBuilder, TreeMutator}; 17 18 /// `AssistContext` allows to apply an assist or check if it could be applied. 19 /// 20 /// Assists use a somewhat over-engineered approach, given the current needs. 21 /// The assists workflow consists of two phases. In the first phase, a user asks 22 /// for the list of available assists. In the second phase, the user picks a 23 /// particular assist and it gets applied. 24 /// 25 /// There are two peculiarities here: 26 /// 27 /// * first, we ideally avoid computing more things then necessary to answer "is 28 /// assist applicable" in the first phase. 29 /// * second, when we are applying assist, we don't have a guarantee that there 30 /// weren't any changes between the point when user asked for assists and when 31 /// they applied a particular assist. So, when applying assist, we need to do 32 /// all the checks from scratch. 33 /// 34 /// To avoid repeating the same code twice for both "check" and "apply" 35 /// functions, we use an approach reminiscent of that of Django's function based 36 /// views dealing with forms. Each assist receives a runtime parameter, 37 /// `resolve`. It first check if an edit is applicable (potentially computing 38 /// info required to compute the actual edit). If it is applicable, and 39 /// `resolve` is `true`, it then computes the actual edit. 40 /// 41 /// So, to implement the original assists workflow, we can first apply each edit 42 /// with `resolve = false`, and then applying the selected edit again, with 43 /// `resolve = true` this time. 44 /// 45 /// Note, however, that we don't actually use such two-phase logic at the 46 /// moment, because the LSP API is pretty awkward in this place, and it's much 47 /// easier to just compute the edit eagerly :-) 48 pub(crate) struct AssistContext<'a> { 49 pub(crate) config: &'a AssistConfig, 50 pub(crate) sema: Semantics<'a, RootDatabase>, 51 frange: FileRange, 52 trimmed_range: TextRange, 53 source_file: SourceFile, 54 } 55 56 impl<'a> AssistContext<'a> { new( sema: Semantics<'a, RootDatabase>, config: &'a AssistConfig, frange: FileRange, ) -> AssistContext<'a>57 pub(crate) fn new( 58 sema: Semantics<'a, RootDatabase>, 59 config: &'a AssistConfig, 60 frange: FileRange, 61 ) -> AssistContext<'a> { 62 let source_file = sema.parse(frange.file_id); 63 64 let start = frange.range.start(); 65 let end = frange.range.end(); 66 let left = source_file.syntax().token_at_offset(start); 67 let right = source_file.syntax().token_at_offset(end); 68 let left = 69 left.right_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Next)); 70 let right = 71 right.left_biased().and_then(|t| algo::skip_whitespace_token(t, Direction::Prev)); 72 let left = left.map(|t| t.text_range().start().clamp(start, end)); 73 let right = right.map(|t| t.text_range().end().clamp(start, end)); 74 75 let trimmed_range = match (left, right) { 76 (Some(left), Some(right)) if left <= right => TextRange::new(left, right), 77 // Selection solely consists of whitespace so just fall back to the original 78 _ => frange.range, 79 }; 80 81 AssistContext { config, sema, frange, source_file, trimmed_range } 82 } 83 db(&self) -> &RootDatabase84 pub(crate) fn db(&self) -> &RootDatabase { 85 self.sema.db 86 } 87 88 // NB, this ignores active selection. offset(&self) -> TextSize89 pub(crate) fn offset(&self) -> TextSize { 90 self.frange.range.start() 91 } 92 file_id(&self) -> FileId93 pub(crate) fn file_id(&self) -> FileId { 94 self.frange.file_id 95 } 96 has_empty_selection(&self) -> bool97 pub(crate) fn has_empty_selection(&self) -> bool { 98 self.trimmed_range.is_empty() 99 } 100 101 /// Returns the selected range trimmed for whitespace tokens, that is the range will be snapped 102 /// to the nearest enclosed token. selection_trimmed(&self) -> TextRange103 pub(crate) fn selection_trimmed(&self) -> TextRange { 104 self.trimmed_range 105 } 106 token_at_offset(&self) -> TokenAtOffset<SyntaxToken>107 pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> { 108 self.source_file.syntax().token_at_offset(self.offset()) 109 } find_token_syntax_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken>110 pub(crate) fn find_token_syntax_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken> { 111 self.token_at_offset().find(|it| it.kind() == kind) 112 } find_token_at_offset<T: AstToken>(&self) -> Option<T>113 pub(crate) fn find_token_at_offset<T: AstToken>(&self) -> Option<T> { 114 self.token_at_offset().find_map(T::cast) 115 } find_node_at_offset<N: AstNode>(&self) -> Option<N>116 pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> { 117 find_node_at_offset(self.source_file.syntax(), self.offset()) 118 } find_node_at_range<N: AstNode>(&self) -> Option<N>119 pub(crate) fn find_node_at_range<N: AstNode>(&self) -> Option<N> { 120 find_node_at_range(self.source_file.syntax(), self.trimmed_range) 121 } find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N>122 pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> { 123 self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset()) 124 } 125 /// Returns the element covered by the selection range, this excludes trailing whitespace in the selection. covering_element(&self) -> SyntaxElement126 pub(crate) fn covering_element(&self) -> SyntaxElement { 127 self.source_file.syntax().covering_element(self.selection_trimmed()) 128 } 129 } 130 131 pub(crate) struct Assists { 132 file: FileId, 133 resolve: AssistResolveStrategy, 134 buf: Vec<Assist>, 135 allowed: Option<Vec<AssistKind>>, 136 } 137 138 impl Assists { new(ctx: &AssistContext<'_>, resolve: AssistResolveStrategy) -> Assists139 pub(crate) fn new(ctx: &AssistContext<'_>, resolve: AssistResolveStrategy) -> Assists { 140 Assists { 141 resolve, 142 file: ctx.frange.file_id, 143 buf: Vec::new(), 144 allowed: ctx.config.allowed.clone(), 145 } 146 } 147 finish(mut self) -> Vec<Assist>148 pub(crate) fn finish(mut self) -> Vec<Assist> { 149 self.buf.sort_by_key(|assist| assist.target.len()); 150 self.buf 151 } 152 add( &mut self, id: AssistId, label: impl Into<String>, target: TextRange, f: impl FnOnce(&mut SourceChangeBuilder), ) -> Option<()>153 pub(crate) fn add( 154 &mut self, 155 id: AssistId, 156 label: impl Into<String>, 157 target: TextRange, 158 f: impl FnOnce(&mut SourceChangeBuilder), 159 ) -> Option<()> { 160 let mut f = Some(f); 161 self.add_impl(None, id, label.into(), target, &mut |it| f.take().unwrap()(it)) 162 } 163 add_group( &mut self, group: &GroupLabel, id: AssistId, label: impl Into<String>, target: TextRange, f: impl FnOnce(&mut SourceChangeBuilder), ) -> Option<()>164 pub(crate) fn add_group( 165 &mut self, 166 group: &GroupLabel, 167 id: AssistId, 168 label: impl Into<String>, 169 target: TextRange, 170 f: impl FnOnce(&mut SourceChangeBuilder), 171 ) -> Option<()> { 172 let mut f = Some(f); 173 self.add_impl(Some(group), id, label.into(), target, &mut |it| f.take().unwrap()(it)) 174 } 175 add_impl( &mut self, group: Option<&GroupLabel>, id: AssistId, label: String, target: TextRange, f: &mut dyn FnMut(&mut SourceChangeBuilder), ) -> Option<()>176 fn add_impl( 177 &mut self, 178 group: Option<&GroupLabel>, 179 id: AssistId, 180 label: String, 181 target: TextRange, 182 f: &mut dyn FnMut(&mut SourceChangeBuilder), 183 ) -> Option<()> { 184 if !self.is_allowed(&id) { 185 return None; 186 } 187 188 let mut trigger_signature_help = false; 189 let source_change = if self.resolve.should_resolve(&id) { 190 let mut builder = SourceChangeBuilder::new(self.file); 191 f(&mut builder); 192 trigger_signature_help = builder.trigger_signature_help; 193 Some(builder.finish()) 194 } else { 195 None 196 }; 197 198 let label = Label::new(label); 199 let group = group.cloned(); 200 self.buf.push(Assist { id, label, group, target, source_change, trigger_signature_help }); 201 Some(()) 202 } 203 is_allowed(&self, id: &AssistId) -> bool204 fn is_allowed(&self, id: &AssistId) -> bool { 205 match &self.allowed { 206 Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)), 207 None => true, 208 } 209 } 210 } 211