From 4867968d22899395e6551f22641b3617e995140c Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 6 May 2020 18:45:35 +0200 Subject: Refactor assists API to be more convenient for adding new assists It now duplicates completion API in its shape. --- crates/ra_assists/src/assist_context.rs | 234 ++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 crates/ra_assists/src/assist_context.rs (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs new file mode 100644 index 000000000..203ad1273 --- /dev/null +++ b/crates/ra_assists/src/assist_context.rs @@ -0,0 +1,234 @@ +//! See `AssistContext` + +use algo::find_covering_element; +use hir::Semantics; +use ra_db::{FileId, FileRange}; +use ra_fmt::{leading_indent, reindent}; +use ra_ide_db::{ + source_change::{SingleFileChange, SourceChange}, + RootDatabase, +}; +use ra_syntax::{ + algo::{self, find_node_at_offset, SyntaxRewriter}, + AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize, + TokenAtOffset, +}; +use ra_text_edit::TextEditBuilder; + +use crate::{AssistId, AssistLabel, GroupLabel, ResolvedAssist}; + +/// `AssistContext` allows to apply an assist or check if it could be applied. +/// +/// Assists use a somewhat over-engineered approach, given the current needs. +/// The assists workflow consists of two phases. In the first phase, a user asks +/// for the list of available assists. In the second phase, the user picks a +/// particular assist and it gets applied. +/// +/// There are two peculiarities here: +/// +/// * first, we ideally avoid computing more things then necessary to answer "is +/// assist applicable" in the first phase. +/// * second, when we are applying assist, we don't have a guarantee that there +/// weren't any changes between the point when user asked for assists and when +/// they applied a particular assist. So, when applying assist, we need to do +/// all the checks from scratch. +/// +/// To avoid repeating the same code twice for both "check" and "apply" +/// functions, we use an approach reminiscent of that of Django's function based +/// views dealing with forms. Each assist receives a runtime parameter, +/// `resolve`. It first check if an edit is applicable (potentially computing +/// info required to compute the actual edit). If it is applicable, and +/// `resolve` is `true`, it then computes the actual edit. +/// +/// So, to implement the original assists workflow, we can first apply each edit +/// with `resolve = false`, and then applying the selected edit again, with +/// `resolve = true` this time. +/// +/// Note, however, that we don't actually use such two-phase logic at the +/// moment, because the LSP API is pretty awkward in this place, and it's much +/// easier to just compute the edit eagerly :-) +pub(crate) struct AssistContext<'a> { + pub(crate) sema: Semantics<'a, RootDatabase>, + pub(super) db: &'a RootDatabase, + pub(crate) frange: FileRange, + source_file: SourceFile, +} + +impl<'a> AssistContext<'a> { + pub fn new(sema: Semantics<'a, RootDatabase>, frange: FileRange) -> AssistContext<'a> { + let source_file = sema.parse(frange.file_id); + let db = sema.db; + AssistContext { sema, db, frange, source_file } + } + + // NB, this ignores active selection. + pub(crate) fn offset(&self) -> TextSize { + self.frange.range.start() + } + + pub(crate) fn token_at_offset(&self) -> TokenAtOffset { + self.source_file.syntax().token_at_offset(self.offset()) + } + pub(crate) fn find_token_at_offset(&self, kind: SyntaxKind) -> Option { + self.token_at_offset().find(|it| it.kind() == kind) + } + pub(crate) fn find_node_at_offset(&self) -> Option { + find_node_at_offset(self.source_file.syntax(), self.offset()) + } + pub(crate) fn find_node_at_offset_with_descend(&self) -> Option { + self.sema + .find_node_at_offset_with_descend(self.source_file.syntax(), self.frange.range.start()) + } + pub(crate) fn covering_element(&self) -> SyntaxElement { + find_covering_element(self.source_file.syntax(), self.frange.range) + } + // FIXME: remove + pub(crate) fn covering_node_for_range(&self, range: TextRange) -> SyntaxElement { + find_covering_element(self.source_file.syntax(), range) + } +} + +pub(crate) struct Assists { + resolve: bool, + file: FileId, + buf: Vec<(AssistLabel, Option)>, +} + +impl Assists { + pub(crate) fn new_resolved(ctx: &AssistContext) -> Assists { + Assists { resolve: true, file: ctx.frange.file_id, buf: Vec::new() } + } + pub(crate) fn new_unresolved(ctx: &AssistContext) -> Assists { + Assists { resolve: false, file: ctx.frange.file_id, buf: Vec::new() } + } + + pub(crate) fn finish_unresolved(self) -> Vec { + assert!(!self.resolve); + self.finish() + .into_iter() + .map(|(label, edit)| { + assert!(edit.is_none()); + label + }) + .collect() + } + + pub(crate) fn finish_resolved(self) -> Vec { + assert!(self.resolve); + self.finish() + .into_iter() + .map(|(label, edit)| ResolvedAssist { label, source_change: edit.unwrap() }) + .collect() + } + + pub(crate) fn add( + &mut self, + id: AssistId, + label: impl Into, + target: TextRange, + f: impl FnOnce(&mut AssistBuilder), + ) -> Option<()> { + let label = AssistLabel::new(id, label.into(), None, target); + self.add_impl(label, f) + } + pub(crate) fn add_group( + &mut self, + group: &GroupLabel, + id: AssistId, + label: impl Into, + target: TextRange, + f: impl FnOnce(&mut AssistBuilder), + ) -> Option<()> { + let label = AssistLabel::new(id, label.into(), Some(group.clone()), target); + self.add_impl(label, f) + } + fn add_impl(&mut self, label: AssistLabel, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> { + let change_label = label.label.clone(); + let source_change = if self.resolve { + let mut builder = AssistBuilder::new(self.file); + f(&mut builder); + Some(builder.finish(change_label)) + } else { + None + }; + + self.buf.push((label, source_change)); + Some(()) + } + + fn finish(mut self) -> Vec<(AssistLabel, Option)> { + self.buf.sort_by_key(|(label, _edit)| label.target.len()); + self.buf + } +} + +pub(crate) struct AssistBuilder { + edit: TextEditBuilder, + cursor_position: Option, + file: FileId, +} + +impl AssistBuilder { + pub(crate) fn new(file: FileId) -> AssistBuilder { + AssistBuilder { edit: TextEditBuilder::default(), cursor_position: None, file } + } + + /// Remove specified `range` of text. + pub(crate) fn delete(&mut self, range: TextRange) { + self.edit.delete(range) + } + /// Append specified `text` at the given `offset` + pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into) { + self.edit.insert(offset, text.into()) + } + /// Replaces specified `range` of text with a given string. + pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into) { + self.edit.replace(range, replace_with.into()) + } + pub(crate) fn replace_ast(&mut self, old: N, new: N) { + algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) + } + /// Replaces specified `node` of text with a given string, reindenting the + /// string to maintain `node`'s existing indent. + // FIXME: remove in favor of ra_syntax::edit::IndentLevel::increase_indent + pub(crate) fn replace_node_and_indent( + &mut self, + node: &SyntaxNode, + replace_with: impl Into, + ) { + let mut replace_with = replace_with.into(); + if let Some(indent) = leading_indent(node) { + replace_with = reindent(&replace_with, &indent) + } + self.replace(node.text_range(), replace_with) + } + pub(crate) fn rewrite(&mut self, rewriter: SyntaxRewriter) { + let node = rewriter.rewrite_root().unwrap(); + let new = rewriter.rewrite(&node); + algo::diff(&node, &new).into_text_edit(&mut self.edit) + } + + /// Specify desired position of the cursor after the assist is applied. + pub(crate) fn set_cursor(&mut self, offset: TextSize) { + self.cursor_position = Some(offset) + } + // FIXME: better API + pub(crate) fn set_file(&mut self, assist_file: FileId) { + self.file = assist_file; + } + + // FIXME: kill this API + /// Get access to the raw `TextEditBuilder`. + pub(crate) fn text_edit_builder(&mut self) -> &mut TextEditBuilder { + &mut self.edit + } + + fn finish(self, change_label: String) -> SourceChange { + let edit = self.edit.finish(); + if edit.is_empty() && self.cursor_position.is_none() { + panic!("Only call `add_assist` if the assist can be applied") + } + SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } + .into_source_change(self.file) + } +} -- cgit v1.2.3 From c6b81bc013b5278b917d109b723405e0df413323 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 7 May 2020 17:09:59 +0200 Subject: Rename AssitLabel -> Assist --- crates/ra_assists/src/assist_context.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 203ad1273..81052ab49 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -15,7 +15,7 @@ use ra_syntax::{ }; use ra_text_edit::TextEditBuilder; -use crate::{AssistId, AssistLabel, GroupLabel, ResolvedAssist}; +use crate::{Assist, AssistId, GroupLabel, ResolvedAssist}; /// `AssistContext` allows to apply an assist or check if it could be applied. /// @@ -91,7 +91,7 @@ impl<'a> AssistContext<'a> { pub(crate) struct Assists { resolve: bool, file: FileId, - buf: Vec<(AssistLabel, Option)>, + buf: Vec<(Assist, Option)>, } impl Assists { @@ -102,7 +102,7 @@ impl Assists { Assists { resolve: false, file: ctx.frange.file_id, buf: Vec::new() } } - pub(crate) fn finish_unresolved(self) -> Vec { + pub(crate) fn finish_unresolved(self) -> Vec { assert!(!self.resolve); self.finish() .into_iter() @@ -117,7 +117,7 @@ impl Assists { assert!(self.resolve); self.finish() .into_iter() - .map(|(label, edit)| ResolvedAssist { label, source_change: edit.unwrap() }) + .map(|(label, edit)| ResolvedAssist { assist: label, source_change: edit.unwrap() }) .collect() } @@ -128,7 +128,7 @@ impl Assists { target: TextRange, f: impl FnOnce(&mut AssistBuilder), ) -> Option<()> { - let label = AssistLabel::new(id, label.into(), None, target); + let label = Assist::new(id, label.into(), None, target); self.add_impl(label, f) } pub(crate) fn add_group( @@ -139,10 +139,10 @@ impl Assists { target: TextRange, f: impl FnOnce(&mut AssistBuilder), ) -> Option<()> { - let label = AssistLabel::new(id, label.into(), Some(group.clone()), target); + let label = Assist::new(id, label.into(), Some(group.clone()), target); self.add_impl(label, f) } - fn add_impl(&mut self, label: AssistLabel, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> { + fn add_impl(&mut self, label: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> { let change_label = label.label.clone(); let source_change = if self.resolve { let mut builder = AssistBuilder::new(self.file); @@ -156,7 +156,7 @@ impl Assists { Some(()) } - fn finish(mut self) -> Vec<(AssistLabel, Option)> { + fn finish(mut self) -> Vec<(Assist, Option)> { self.buf.sort_by_key(|(label, _edit)| label.target.len()); self.buf } -- cgit v1.2.3 From 1e790ea3149f085e49cb66e6a052920f72da01e9 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 7 May 2020 17:32:01 +0200 Subject: Simplify --- crates/ra_assists/src/assist_context.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 81052ab49..3085c4330 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -76,8 +76,7 @@ impl<'a> AssistContext<'a> { find_node_at_offset(self.source_file.syntax(), self.offset()) } pub(crate) fn find_node_at_offset_with_descend(&self) -> Option { - self.sema - .find_node_at_offset_with_descend(self.source_file.syntax(), self.frange.range.start()) + self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset()) } pub(crate) fn covering_element(&self) -> SyntaxElement { find_covering_element(self.source_file.syntax(), self.frange.range) -- cgit v1.2.3 From c6334285e3b8369ca685ca436d94ff1f64201044 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 9 May 2020 13:57:19 +0200 Subject: Fix visibility --- crates/ra_assists/src/assist_context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 3085c4330..a680f752b 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -49,7 +49,7 @@ use crate::{Assist, AssistId, GroupLabel, ResolvedAssist}; /// easier to just compute the edit eagerly :-) pub(crate) struct AssistContext<'a> { pub(crate) sema: Semantics<'a, RootDatabase>, - pub(super) db: &'a RootDatabase, + pub(crate) db: &'a RootDatabase, pub(crate) frange: FileRange, source_file: SourceFile, } -- cgit v1.2.3 From c847c079fd66d7ed09c64ff398baf05317b16500 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 17 May 2020 12:09:53 +0200 Subject: Add AssistConfig --- crates/ra_assists/src/assist_context.rs | 51 +++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index a680f752b..b90bbf8b2 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -15,7 +15,10 @@ use ra_syntax::{ }; 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. /// @@ -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 /// easier to just compute the edit eagerly :-) pub(crate) struct AssistContext<'a> { + pub(crate) config: &'a AssistConfig, pub(crate) sema: Semantics<'a, RootDatabase>, pub(crate) db: &'a RootDatabase, pub(crate) frange: FileRange, @@ -55,10 +59,14 @@ pub(crate) struct 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 db = sema.db; - AssistContext { sema, db, frange, source_file } + AssistContext { config, sema, db, frange, source_file } } // NB, this ignores active selection. @@ -165,11 +173,17 @@ pub(crate) struct AssistBuilder { edit: TextEditBuilder, cursor_position: Option, file: FileId, + is_snippet: bool, } impl 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. @@ -180,10 +194,30 @@ impl AssistBuilder { pub(crate) fn insert(&mut self, offset: TextSize, text: impl 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, + ) { + self.is_snippet = true; + self.edit.insert(offset, text.into()) + } /// Replaces specified `range` of text with a given string. pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl 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, + ) { + self.is_snippet = true; + self.edit.replace(range, replace_with.into()) + } pub(crate) fn replace_ast(&mut self, old: N, new: N) { 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() { panic!("Only call `add_assist` if the assist can be applied") } - SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } - .into_source_change(self.file) + let mut res = + SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } + .into_source_change(self.file); + if self.is_snippet { + res.is_snippet = true; + } + res } } -- cgit v1.2.3 From 2bf6b16a7f174ea3f581f26f642b4febff0b9ce8 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 18 May 2020 00:11:40 +0200 Subject: Server side of SnippetTextEdit --- crates/ra_assists/src/assist_context.rs | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index b90bbf8b2..0dcd9df61 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -208,16 +208,6 @@ impl AssistBuilder { pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl 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, - ) { - self.is_snippet = true; - self.edit.replace(range, replace_with.into()) - } pub(crate) fn replace_ast(&mut self, old: N, new: N) { algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) } -- cgit v1.2.3 From a04cababaa144d7a6db7b1dd114494b33d281ab9 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 20 May 2020 01:53:21 +0200 Subject: Use snippets in add_missing_members --- crates/ra_assists/src/assist_context.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 0dcd9df61..005c17776 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -194,20 +194,30 @@ impl AssistBuilder { pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into) { self.edit.insert(offset, text.into()) } - /// Append specified `text` at the given `offset` + /// Append specified `snippet` at the given `offset` pub(crate) fn insert_snippet( &mut self, _cap: SnippetCap, offset: TextSize, - text: impl Into, + snippet: impl Into, ) { self.is_snippet = true; - self.edit.insert(offset, text.into()) + self.insert(offset, snippet); } /// Replaces specified `range` of text with a given string. pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into) { self.edit.replace(range, replace_with.into()) } + /// Replaces specified `range` of text with a given `snippet`. + pub(crate) fn replace_snippet( + &mut self, + _cap: SnippetCap, + range: TextRange, + snippet: impl Into, + ) { + self.is_snippet = true; + self.replace(range, snippet); + } pub(crate) fn replace_ast(&mut self, old: N, new: N) { algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit) } -- cgit v1.2.3 From 70930d3bb2ba1d4a7a7d4a489da714096294acca Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 21 May 2020 00:03:42 +0200 Subject: Remove set_cursor --- crates/ra_assists/src/assist_context.rs | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 005c17776..9f6ca449b 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -171,19 +171,13 @@ impl Assists { pub(crate) struct AssistBuilder { edit: TextEditBuilder, - cursor_position: Option, file: FileId, is_snippet: bool, } impl AssistBuilder { pub(crate) fn new(file: FileId) -> AssistBuilder { - AssistBuilder { - edit: TextEditBuilder::default(), - cursor_position: None, - file, - is_snippet: false, - } + AssistBuilder { edit: TextEditBuilder::default(), file, is_snippet: false } } /// Remove specified `range` of text. @@ -241,10 +235,6 @@ impl AssistBuilder { algo::diff(&node, &new).into_text_edit(&mut self.edit) } - /// Specify desired position of the cursor after the assist is applied. - pub(crate) fn set_cursor(&mut self, offset: TextSize) { - self.cursor_position = Some(offset) - } // FIXME: better API pub(crate) fn set_file(&mut self, assist_file: FileId) { self.file = assist_file; @@ -258,12 +248,8 @@ impl AssistBuilder { fn finish(self, change_label: String) -> SourceChange { let edit = self.edit.finish(); - if edit.is_empty() && self.cursor_position.is_none() { - panic!("Only call `add_assist` if the assist can be applied") - } - let mut res = - SingleFileChange { label: change_label, edit, cursor_position: self.cursor_position } - .into_source_change(self.file); + let mut res = SingleFileChange { label: change_label, edit, cursor_position: None } + .into_source_change(self.file); if self.is_snippet { res.is_snippet = true; } -- cgit v1.2.3 From 4fdb1eac08bc29029fe888967dcc11d38d25c205 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 21 May 2020 00:46:08 +0200 Subject: Remove unused cursor positions --- crates/ra_assists/src/assist_context.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'crates/ra_assists/src/assist_context.rs') diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 9f6ca449b..f3af70a3e 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs @@ -248,8 +248,7 @@ impl AssistBuilder { fn finish(self, change_label: String) -> SourceChange { let edit = self.edit.finish(); - let mut res = SingleFileChange { label: change_label, edit, cursor_position: None } - .into_source_change(self.file); + let mut res = SingleFileChange { label: change_label, edit }.into_source_change(self.file); if self.is_snippet { res.is_snippet = true; } -- cgit v1.2.3