]> git.lizzy.rs Git - rust.git/blob - crates/rust-analyzer/src/conv.rs
More type safety for highlighting
[rust.git] / crates / rust-analyzer / src / conv.rs
1 //! Convenience module responsible for translating between rust-analyzer's types
2 //! and LSP types.
3
4 use lsp_types::{
5     self, CreateFile, DiagnosticSeverity, DocumentChangeOperation, DocumentChanges, Documentation,
6     Location, LocationLink, MarkupContent, MarkupKind, Position, Range, RenameFile, ResourceOp,
7     SemanticTokenModifier, SemanticTokenType, SymbolKind, TextDocumentEdit, TextDocumentIdentifier,
8     TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier,
9     WorkspaceEdit,
10 };
11 use ra_ide::{
12     translate_offset_with_edit, CompletionItem, CompletionItemKind, FileId, FilePosition,
13     FileRange, FileSystemEdit, Fold, FoldKind, HighlightTag, InsertTextFormat, LineCol, LineIndex,
14     NavigationTarget, RangeInfo, ReferenceAccess, Severity, SourceChange, SourceFileEdit,
15 };
16 use ra_syntax::{SyntaxKind, TextRange, TextUnit};
17 use ra_text_edit::{AtomTextEdit, TextEdit};
18 use ra_vfs::LineEndings;
19
20 use crate::{req, semantic_tokens, world::WorldSnapshot, Result};
21
22 pub trait Conv {
23     type Output;
24     fn conv(self) -> Self::Output;
25 }
26
27 pub trait ConvWith<CTX> {
28     type Output;
29     fn conv_with(self, ctx: CTX) -> Self::Output;
30 }
31
32 pub trait TryConvWith<CTX> {
33     type Output;
34     fn try_conv_with(self, ctx: CTX) -> Result<Self::Output>;
35 }
36
37 impl Conv for SyntaxKind {
38     type Output = SymbolKind;
39
40     fn conv(self) -> <Self as Conv>::Output {
41         match self {
42             SyntaxKind::FN_DEF => SymbolKind::Function,
43             SyntaxKind::STRUCT_DEF => SymbolKind::Struct,
44             SyntaxKind::ENUM_DEF => SymbolKind::Enum,
45             SyntaxKind::ENUM_VARIANT => SymbolKind::EnumMember,
46             SyntaxKind::TRAIT_DEF => SymbolKind::Interface,
47             SyntaxKind::MACRO_CALL => SymbolKind::Function,
48             SyntaxKind::MODULE => SymbolKind::Module,
49             SyntaxKind::TYPE_ALIAS_DEF => SymbolKind::TypeParameter,
50             SyntaxKind::RECORD_FIELD_DEF => SymbolKind::Field,
51             SyntaxKind::STATIC_DEF => SymbolKind::Constant,
52             SyntaxKind::CONST_DEF => SymbolKind::Constant,
53             SyntaxKind::IMPL_BLOCK => SymbolKind::Object,
54             _ => SymbolKind::Variable,
55         }
56     }
57 }
58
59 impl Conv for ReferenceAccess {
60     type Output = ::lsp_types::DocumentHighlightKind;
61
62     fn conv(self) -> Self::Output {
63         use lsp_types::DocumentHighlightKind;
64         match self {
65             ReferenceAccess::Read => DocumentHighlightKind::Read,
66             ReferenceAccess::Write => DocumentHighlightKind::Write,
67         }
68     }
69 }
70
71 impl Conv for CompletionItemKind {
72     type Output = ::lsp_types::CompletionItemKind;
73
74     fn conv(self) -> <Self as Conv>::Output {
75         use lsp_types::CompletionItemKind::*;
76         match self {
77             CompletionItemKind::Keyword => Keyword,
78             CompletionItemKind::Snippet => Snippet,
79             CompletionItemKind::Module => Module,
80             CompletionItemKind::Function => Function,
81             CompletionItemKind::Struct => Struct,
82             CompletionItemKind::Enum => Enum,
83             CompletionItemKind::EnumVariant => EnumMember,
84             CompletionItemKind::BuiltinType => Struct,
85             CompletionItemKind::Binding => Variable,
86             CompletionItemKind::Field => Field,
87             CompletionItemKind::Trait => Interface,
88             CompletionItemKind::TypeAlias => Struct,
89             CompletionItemKind::Const => Constant,
90             CompletionItemKind::Static => Value,
91             CompletionItemKind::Method => Method,
92             CompletionItemKind::TypeParam => TypeParameter,
93             CompletionItemKind::Macro => Method,
94         }
95     }
96 }
97
98 impl Conv for Severity {
99     type Output = DiagnosticSeverity;
100     fn conv(self) -> DiagnosticSeverity {
101         match self {
102             Severity::Error => DiagnosticSeverity::Error,
103             Severity::WeakWarning => DiagnosticSeverity::Hint,
104         }
105     }
106 }
107
108 impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem {
109     type Output = ::lsp_types::CompletionItem;
110
111     fn conv_with(self, ctx: (&LineIndex, LineEndings)) -> ::lsp_types::CompletionItem {
112         let mut additional_text_edits = Vec::new();
113         let mut text_edit = None;
114         // LSP does not allow arbitrary edits in completion, so we have to do a
115         // non-trivial mapping here.
116         for atom_edit in self.text_edit().as_atoms() {
117             if self.source_range().is_subrange(&atom_edit.delete) {
118                 text_edit = Some(if atom_edit.delete == self.source_range() {
119                     atom_edit.conv_with(ctx)
120                 } else {
121                     assert!(self.source_range().end() == atom_edit.delete.end());
122                     let range1 =
123                         TextRange::from_to(atom_edit.delete.start(), self.source_range().start());
124                     let range2 = self.source_range();
125                     let edit1 = AtomTextEdit::replace(range1, String::new());
126                     let edit2 = AtomTextEdit::replace(range2, atom_edit.insert.clone());
127                     additional_text_edits.push(edit1.conv_with(ctx));
128                     edit2.conv_with(ctx)
129                 })
130             } else {
131                 assert!(self.source_range().intersection(&atom_edit.delete).is_none());
132                 additional_text_edits.push(atom_edit.conv_with(ctx));
133             }
134         }
135         let text_edit = text_edit.unwrap();
136
137         let mut res = lsp_types::CompletionItem {
138             label: self.label().to_string(),
139             detail: self.detail().map(|it| it.to_string()),
140             filter_text: Some(self.lookup().to_string()),
141             kind: self.kind().map(|it| it.conv()),
142             text_edit: Some(text_edit),
143             additional_text_edits: Some(additional_text_edits),
144             documentation: self.documentation().map(|it| it.conv()),
145             deprecated: Some(self.deprecated()),
146             ..Default::default()
147         };
148
149         if self.deprecated() {
150             res.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated])
151         }
152
153         res.insert_text_format = Some(match self.insert_text_format() {
154             InsertTextFormat::Snippet => lsp_types::InsertTextFormat::Snippet,
155             InsertTextFormat::PlainText => lsp_types::InsertTextFormat::PlainText,
156         });
157
158         res
159     }
160 }
161
162 impl ConvWith<&LineIndex> for Position {
163     type Output = TextUnit;
164
165     fn conv_with(self, line_index: &LineIndex) -> TextUnit {
166         let line_col = LineCol { line: self.line as u32, col_utf16: self.character as u32 };
167         line_index.offset(line_col)
168     }
169 }
170
171 impl ConvWith<&LineIndex> for TextUnit {
172     type Output = Position;
173
174     fn conv_with(self, line_index: &LineIndex) -> Position {
175         let line_col = line_index.line_col(self);
176         Position::new(u64::from(line_col.line), u64::from(line_col.col_utf16))
177     }
178 }
179
180 impl ConvWith<&LineIndex> for TextRange {
181     type Output = Range;
182
183     fn conv_with(self, line_index: &LineIndex) -> Range {
184         Range::new(self.start().conv_with(line_index), self.end().conv_with(line_index))
185     }
186 }
187
188 impl ConvWith<&LineIndex> for Range {
189     type Output = TextRange;
190
191     fn conv_with(self, line_index: &LineIndex) -> TextRange {
192         TextRange::from_to(self.start.conv_with(line_index), self.end.conv_with(line_index))
193     }
194 }
195
196 impl Conv for ra_ide::Documentation {
197     type Output = lsp_types::Documentation;
198     fn conv(self) -> Documentation {
199         Documentation::MarkupContent(MarkupContent {
200             kind: MarkupKind::Markdown,
201             value: crate::markdown::format_docs(self.as_str()),
202         })
203     }
204 }
205
206 impl Conv for ra_ide::FunctionSignature {
207     type Output = lsp_types::SignatureInformation;
208     fn conv(self) -> Self::Output {
209         use lsp_types::{ParameterInformation, ParameterLabel, SignatureInformation};
210
211         let label = self.to_string();
212
213         let documentation = self.doc.map(|it| it.conv());
214
215         let parameters: Vec<ParameterInformation> = self
216             .parameters
217             .into_iter()
218             .map(|param| ParameterInformation {
219                 label: ParameterLabel::Simple(param),
220                 documentation: None,
221             })
222             .collect();
223
224         SignatureInformation { label, documentation, parameters: Some(parameters) }
225     }
226 }
227
228 impl ConvWith<(&LineIndex, LineEndings)> for TextEdit {
229     type Output = Vec<lsp_types::TextEdit>;
230
231     fn conv_with(self, ctx: (&LineIndex, LineEndings)) -> Vec<lsp_types::TextEdit> {
232         self.as_atoms().iter().map_conv_with(ctx).collect()
233     }
234 }
235
236 impl ConvWith<(&LineIndex, LineEndings)> for &AtomTextEdit {
237     type Output = lsp_types::TextEdit;
238
239     fn conv_with(
240         self,
241         (line_index, line_endings): (&LineIndex, LineEndings),
242     ) -> lsp_types::TextEdit {
243         let mut new_text = self.insert.clone();
244         if line_endings == LineEndings::Dos {
245             new_text = new_text.replace('\n', "\r\n");
246         }
247         lsp_types::TextEdit { range: self.delete.conv_with(line_index), new_text }
248     }
249 }
250
251 pub(crate) struct FoldConvCtx<'a> {
252     pub(crate) text: &'a str,
253     pub(crate) line_index: &'a LineIndex,
254     pub(crate) line_folding_only: bool,
255 }
256
257 impl ConvWith<&FoldConvCtx<'_>> for Fold {
258     type Output = lsp_types::FoldingRange;
259
260     fn conv_with(self, ctx: &FoldConvCtx) -> lsp_types::FoldingRange {
261         let kind = match self.kind {
262             FoldKind::Comment => Some(lsp_types::FoldingRangeKind::Comment),
263             FoldKind::Imports => Some(lsp_types::FoldingRangeKind::Imports),
264             FoldKind::Mods => None,
265             FoldKind::Block => None,
266         };
267
268         let range = self.range.conv_with(&ctx.line_index);
269
270         if ctx.line_folding_only {
271             // Clients with line_folding_only == true (such as VSCode) will fold the whole end line
272             // even if it contains text not in the folding range. To prevent that we exclude
273             // range.end.line from the folding region if there is more text after range.end
274             // on the same line.
275             let has_more_text_on_end_line = ctx.text
276                 [TextRange::from_to(self.range.end(), TextUnit::of_str(ctx.text))]
277             .chars()
278             .take_while(|it| *it != '\n')
279             .any(|it| !it.is_whitespace());
280
281             let end_line = if has_more_text_on_end_line {
282                 range.end.line.saturating_sub(1)
283             } else {
284                 range.end.line
285             };
286
287             lsp_types::FoldingRange {
288                 start_line: range.start.line,
289                 start_character: None,
290                 end_line,
291                 end_character: None,
292                 kind,
293             }
294         } else {
295             lsp_types::FoldingRange {
296                 start_line: range.start.line,
297                 start_character: Some(range.start.character),
298                 end_line: range.end.line,
299                 end_character: Some(range.end.character),
300                 kind,
301             }
302         }
303     }
304 }
305
306 impl Conv for HighlightTag {
307     type Output = (SemanticTokenType, Vec<SemanticTokenModifier>);
308
309     fn conv(self) -> (SemanticTokenType, Vec<SemanticTokenModifier>) {
310         let token_type: SemanticTokenType = match self {
311             HighlightTag::FIELD => SemanticTokenType::MEMBER,
312             HighlightTag::FUNCTION => SemanticTokenType::FUNCTION,
313             HighlightTag::MODULE => SemanticTokenType::NAMESPACE,
314             HighlightTag::CONSTANT => {
315                 return (
316                     SemanticTokenType::VARIABLE,
317                     vec![SemanticTokenModifier::STATIC, SemanticTokenModifier::READONLY],
318                 )
319             }
320             HighlightTag::MACRO => SemanticTokenType::MACRO,
321
322             HighlightTag::VARIABLE => {
323                 return (SemanticTokenType::VARIABLE, vec![SemanticTokenModifier::READONLY])
324             }
325             HighlightTag::VARIABLE_MUT => SemanticTokenType::VARIABLE,
326
327             HighlightTag::TYPE => SemanticTokenType::TYPE,
328             HighlightTag::TYPE_BUILTIN => SemanticTokenType::TYPE,
329             HighlightTag::TYPE_SELF => {
330                 return (SemanticTokenType::TYPE, vec![SemanticTokenModifier::REFERENCE])
331             }
332             HighlightTag::TYPE_PARAM => SemanticTokenType::TYPE_PARAMETER,
333             HighlightTag::TYPE_LIFETIME => {
334                 return (SemanticTokenType::LABEL, vec![SemanticTokenModifier::REFERENCE])
335             }
336
337             HighlightTag::LITERAL_BYTE => SemanticTokenType::NUMBER,
338             HighlightTag::LITERAL_NUMERIC => SemanticTokenType::NUMBER,
339             HighlightTag::LITERAL_CHAR => SemanticTokenType::NUMBER,
340
341             HighlightTag::LITERAL_COMMENT => {
342                 return (SemanticTokenType::COMMENT, vec![SemanticTokenModifier::DOCUMENTATION])
343             }
344
345             HighlightTag::LITERAL_STRING => SemanticTokenType::STRING,
346             HighlightTag::LITERAL_ATTRIBUTE => SemanticTokenType::KEYWORD,
347
348             HighlightTag::KEYWORD => SemanticTokenType::KEYWORD,
349             HighlightTag::KEYWORD_UNSAFE => SemanticTokenType::KEYWORD,
350             HighlightTag::KEYWORD_CONTROL => SemanticTokenType::KEYWORD,
351             unknown => panic!("Unknown semantic token: {}", unknown),
352         };
353
354         (token_type, vec![])
355     }
356 }
357
358 impl Conv for (SemanticTokenType, Vec<SemanticTokenModifier>) {
359     type Output = (u32, u32);
360
361     fn conv(self) -> Self::Output {
362         let token_index =
363             semantic_tokens::supported_token_types().iter().position(|it| *it == self.0).unwrap();
364         let mut token_modifier_bitset = 0;
365         for modifier in self.1.iter() {
366             token_modifier_bitset |= semantic_tokens::supported_token_modifiers()
367                 .iter()
368                 .position(|it| it == modifier)
369                 .unwrap();
370         }
371
372         (token_index as u32, token_modifier_bitset as u32)
373     }
374 }
375
376 impl<T: ConvWith<CTX>, CTX> ConvWith<CTX> for Option<T> {
377     type Output = Option<T::Output>;
378
379     fn conv_with(self, ctx: CTX) -> Self::Output {
380         self.map(|x| ConvWith::conv_with(x, ctx))
381     }
382 }
383
384 impl TryConvWith<&WorldSnapshot> for &Url {
385     type Output = FileId;
386     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
387         world.uri_to_file_id(self)
388     }
389 }
390
391 impl TryConvWith<&WorldSnapshot> for FileId {
392     type Output = Url;
393     fn try_conv_with(self, world: &WorldSnapshot) -> Result<Url> {
394         world.file_id_to_uri(self)
395     }
396 }
397
398 impl TryConvWith<&WorldSnapshot> for &TextDocumentItem {
399     type Output = FileId;
400     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
401         self.uri.try_conv_with(world)
402     }
403 }
404
405 impl TryConvWith<&WorldSnapshot> for &VersionedTextDocumentIdentifier {
406     type Output = FileId;
407     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
408         self.uri.try_conv_with(world)
409     }
410 }
411
412 impl TryConvWith<&WorldSnapshot> for &TextDocumentIdentifier {
413     type Output = FileId;
414     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
415         world.uri_to_file_id(&self.uri)
416     }
417 }
418
419 impl TryConvWith<&WorldSnapshot> for &TextDocumentPositionParams {
420     type Output = FilePosition;
421     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FilePosition> {
422         let file_id = self.text_document.try_conv_with(world)?;
423         let line_index = world.analysis().file_line_index(file_id)?;
424         let offset = self.position.conv_with(&line_index);
425         Ok(FilePosition { file_id, offset })
426     }
427 }
428
429 impl TryConvWith<&WorldSnapshot> for (&TextDocumentIdentifier, Range) {
430     type Output = FileRange;
431     fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileRange> {
432         let file_id = self.0.try_conv_with(world)?;
433         let line_index = world.analysis().file_line_index(file_id)?;
434         let range = self.1.conv_with(&line_index);
435         Ok(FileRange { file_id, range })
436     }
437 }
438
439 impl<T: TryConvWith<CTX>, CTX: Copy> TryConvWith<CTX> for Vec<T> {
440     type Output = Vec<<T as TryConvWith<CTX>>::Output>;
441     fn try_conv_with(self, ctx: CTX) -> Result<Self::Output> {
442         let mut res = Vec::with_capacity(self.len());
443         for item in self {
444             res.push(item.try_conv_with(ctx)?);
445         }
446         Ok(res)
447     }
448 }
449
450 impl TryConvWith<&WorldSnapshot> for SourceChange {
451     type Output = req::SourceChange;
452     fn try_conv_with(self, world: &WorldSnapshot) -> Result<req::SourceChange> {
453         let cursor_position = match self.cursor_position {
454             None => None,
455             Some(pos) => {
456                 let line_index = world.analysis().file_line_index(pos.file_id)?;
457                 let edit = self
458                     .source_file_edits
459                     .iter()
460                     .find(|it| it.file_id == pos.file_id)
461                     .map(|it| &it.edit);
462                 let line_col = match edit {
463                     Some(edit) => translate_offset_with_edit(&*line_index, pos.offset, edit),
464                     None => line_index.line_col(pos.offset),
465                 };
466                 let position =
467                     Position::new(u64::from(line_col.line), u64::from(line_col.col_utf16));
468                 Some(TextDocumentPositionParams {
469                     text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?),
470                     position,
471                 })
472             }
473         };
474         let mut document_changes: Vec<DocumentChangeOperation> = Vec::new();
475         for resource_op in self.file_system_edits.try_conv_with(world)? {
476             document_changes.push(DocumentChangeOperation::Op(resource_op));
477         }
478         for text_document_edit in self.source_file_edits.try_conv_with(world)? {
479             document_changes.push(DocumentChangeOperation::Edit(text_document_edit));
480         }
481         let workspace_edit = WorkspaceEdit {
482             changes: None,
483             document_changes: Some(DocumentChanges::Operations(document_changes)),
484         };
485         Ok(req::SourceChange { label: self.label, workspace_edit, cursor_position })
486     }
487 }
488
489 impl TryConvWith<&WorldSnapshot> for SourceFileEdit {
490     type Output = TextDocumentEdit;
491     fn try_conv_with(self, world: &WorldSnapshot) -> Result<TextDocumentEdit> {
492         let text_document = VersionedTextDocumentIdentifier {
493             uri: self.file_id.try_conv_with(world)?,
494             version: None,
495         };
496         let line_index = world.analysis().file_line_index(self.file_id)?;
497         let line_endings = world.file_line_endings(self.file_id);
498         let edits =
499             self.edit.as_atoms().iter().map_conv_with((&line_index, line_endings)).collect();
500         Ok(TextDocumentEdit { text_document, edits })
501     }
502 }
503
504 impl TryConvWith<&WorldSnapshot> for FileSystemEdit {
505     type Output = ResourceOp;
506     fn try_conv_with(self, world: &WorldSnapshot) -> Result<ResourceOp> {
507         let res = match self {
508             FileSystemEdit::CreateFile { source_root, path } => {
509                 let uri = world.path_to_uri(source_root, &path)?;
510                 ResourceOp::Create(CreateFile { uri, options: None })
511             }
512             FileSystemEdit::MoveFile { src, dst_source_root, dst_path } => {
513                 let old_uri = world.file_id_to_uri(src)?;
514                 let new_uri = world.path_to_uri(dst_source_root, &dst_path)?;
515                 ResourceOp::Rename(RenameFile { old_uri, new_uri, options: None })
516             }
517         };
518         Ok(res)
519     }
520 }
521
522 impl TryConvWith<&WorldSnapshot> for &NavigationTarget {
523     type Output = Location;
524     fn try_conv_with(self, world: &WorldSnapshot) -> Result<Location> {
525         let line_index = world.analysis().file_line_index(self.file_id())?;
526         let range = self.range();
527         to_location(self.file_id(), range, &world, &line_index)
528     }
529 }
530
531 impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<NavigationTarget>) {
532     type Output = LocationLink;
533     fn try_conv_with(self, world: &WorldSnapshot) -> Result<LocationLink> {
534         let (src_file_id, target) = self;
535
536         let target_uri = target.info.file_id().try_conv_with(world)?;
537         let src_line_index = world.analysis().file_line_index(src_file_id)?;
538         let tgt_line_index = world.analysis().file_line_index(target.info.file_id())?;
539
540         let target_range = target.info.full_range().conv_with(&tgt_line_index);
541
542         let target_selection_range = target
543             .info
544             .focus_range()
545             .map(|it| it.conv_with(&tgt_line_index))
546             .unwrap_or(target_range);
547
548         let res = LocationLink {
549             origin_selection_range: Some(target.range.conv_with(&src_line_index)),
550             target_uri,
551             target_range,
552             target_selection_range,
553         };
554         Ok(res)
555     }
556 }
557
558 impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<Vec<NavigationTarget>>) {
559     type Output = req::GotoDefinitionResponse;
560     fn try_conv_with(self, world: &WorldSnapshot) -> Result<req::GotoTypeDefinitionResponse> {
561         let (file_id, RangeInfo { range, info: navs }) = self;
562         let links = navs
563             .into_iter()
564             .map(|nav| (file_id, RangeInfo::new(range, nav)))
565             .try_conv_with_to_vec(world)?;
566         if world.options.supports_location_link {
567             Ok(links.into())
568         } else {
569             let locations: Vec<Location> = links
570                 .into_iter()
571                 .map(|link| Location { uri: link.target_uri, range: link.target_selection_range })
572                 .collect();
573             Ok(locations.into())
574         }
575     }
576 }
577
578 pub fn to_call_hierarchy_item(
579     file_id: FileId,
580     range: TextRange,
581     world: &WorldSnapshot,
582     line_index: &LineIndex,
583     nav: NavigationTarget,
584 ) -> Result<lsp_types::CallHierarchyItem> {
585     Ok(lsp_types::CallHierarchyItem {
586         name: nav.name().to_string(),
587         kind: nav.kind().conv(),
588         tags: None,
589         detail: nav.description().map(|it| it.to_string()),
590         uri: file_id.try_conv_with(&world)?,
591         range: nav.range().conv_with(&line_index),
592         selection_range: range.conv_with(&line_index),
593     })
594 }
595
596 pub fn to_location(
597     file_id: FileId,
598     range: TextRange,
599     world: &WorldSnapshot,
600     line_index: &LineIndex,
601 ) -> Result<Location> {
602     let url = file_id.try_conv_with(world)?;
603     let loc = Location::new(url, range.conv_with(line_index));
604     Ok(loc)
605 }
606
607 pub trait MapConvWith<CTX>: Sized {
608     type Output;
609
610     fn map_conv_with(self, ctx: CTX) -> ConvWithIter<Self, CTX> {
611         ConvWithIter { iter: self, ctx }
612     }
613 }
614
615 impl<CTX, I> MapConvWith<CTX> for I
616 where
617     I: Iterator,
618     I::Item: ConvWith<CTX>,
619 {
620     type Output = <I::Item as ConvWith<CTX>>::Output;
621 }
622
623 pub struct ConvWithIter<I, CTX> {
624     iter: I,
625     ctx: CTX,
626 }
627
628 impl<I, CTX> Iterator for ConvWithIter<I, CTX>
629 where
630     I: Iterator,
631     I::Item: ConvWith<CTX>,
632     CTX: Copy,
633 {
634     type Item = <I::Item as ConvWith<CTX>>::Output;
635
636     fn next(&mut self) -> Option<Self::Item> {
637         self.iter.next().map(|item| item.conv_with(self.ctx))
638     }
639 }
640
641 pub trait TryConvWithToVec<CTX>: Sized {
642     type Output;
643
644     fn try_conv_with_to_vec(self, ctx: CTX) -> Result<Vec<Self::Output>>;
645 }
646
647 impl<I, CTX> TryConvWithToVec<CTX> for I
648 where
649     I: Iterator,
650     I::Item: TryConvWith<CTX>,
651     CTX: Copy,
652 {
653     type Output = <I::Item as TryConvWith<CTX>>::Output;
654
655     fn try_conv_with_to_vec(self, ctx: CTX) -> Result<Vec<Self::Output>> {
656         self.map(|it| it.try_conv_with(ctx)).collect()
657     }
658 }
659
660 #[cfg(test)]
661 mod tests {
662     use super::*;
663     use test_utils::extract_ranges;
664
665     #[test]
666     fn conv_fold_line_folding_only_fixup() {
667         let text = r#"<fold>mod a;
668 mod b;
669 mod c;</fold>
670
671 fn main() <fold>{
672     if cond <fold>{
673         a::do_a();
674     }</fold> else <fold>{
675         b::do_b();
676     }</fold>
677 }</fold>"#;
678
679         let (ranges, text) = extract_ranges(text, "fold");
680         assert_eq!(ranges.len(), 4);
681         let folds = vec![
682             Fold { range: ranges[0], kind: FoldKind::Mods },
683             Fold { range: ranges[1], kind: FoldKind::Block },
684             Fold { range: ranges[2], kind: FoldKind::Block },
685             Fold { range: ranges[3], kind: FoldKind::Block },
686         ];
687
688         let line_index = LineIndex::new(&text);
689         let ctx = FoldConvCtx { text: &text, line_index: &line_index, line_folding_only: true };
690         let converted: Vec<_> = folds.into_iter().map_conv_with(&ctx).collect();
691
692         let expected_lines = [(0, 2), (4, 10), (5, 6), (7, 9)];
693         assert_eq!(converted.len(), expected_lines.len());
694         for (folding_range, (start_line, end_line)) in converted.iter().zip(expected_lines.iter()) {
695             assert_eq!(folding_range.start_line, *start_line);
696             assert_eq!(folding_range.start_character, None);
697             assert_eq!(folding_range.end_line, *end_line);
698             assert_eq!(folding_range.end_character, None);
699         }
700     }
701 }