Skip to content

Commit 0524f4b

Browse files
committed
Fix reorder mode dropping trailing comments and reordering comments in exported declarations
Close #158 Close #159
1 parent 4fea64a commit 0524f4b

File tree

5 files changed

+150
-14
lines changed

5 files changed

+150
-14
lines changed

src/reorder.rs

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ struct ClassifiedElement<'a> {
8888
reorderable_element: Option<GDScriptTokenKind>,
8989
}
9090

91+
/// We use this little container to remember comments or annotations that appear
92+
/// before a declaration while we scan the syntax tree from top to bottom.
93+
///
94+
/// The `start_byte` helps us preserve the original order when we later rebuild
95+
/// and attach the snippets to the declaration that follows.
96+
#[derive(Debug, Clone)]
97+
struct PendingAttachment {
98+
start_byte: usize,
99+
text: String,
100+
}
101+
91102
/// This constant lists built-in virtual methods in the order they should appear.
92103
/// The higher the method is in the list, the higher the priority (i.e. _init comes before _ready).
93104
const BUILTIN_VIRTUAL_METHODS: &[&str] = &[
@@ -191,6 +202,24 @@ fn get_token_kind(token_kind: &GDScriptTokenKind) -> TokenKind {
191202
}
192203
}
193204

205+
/// This function combines annotations and comments that were collected while
206+
/// scanning the syntax tree, sorts them by their original position in the
207+
/// source code, and returns them as a list of strings in the original source
208+
/// code order.
209+
fn merge_pending_texts(
210+
pending_annotations: &[PendingAttachment],
211+
pending_comments: &[PendingAttachment],
212+
) -> Vec<String> {
213+
let mut merged: Vec<&PendingAttachment> = pending_annotations.iter().collect();
214+
merged.extend(pending_comments.iter());
215+
merged.sort_by_key(|attachment| attachment.start_byte);
216+
217+
merged
218+
.into_iter()
219+
.map(|attachment| attachment.text.clone())
220+
.collect()
221+
}
222+
194223
/// Extracts all top-level elements from the parsed tree.
195224
fn extract_tokens_to_reorder(
196225
tree: &Tree,
@@ -317,11 +346,17 @@ fn extract_tokens_to_reorder(
317346
if class_docstring_comments_rows.contains(&node.start_position().row) {
318347
continue;
319348
} else {
320-
pending_comments.push(text);
349+
pending_comments.push(PendingAttachment {
350+
start_byte: node.start_byte(),
351+
text: text.clone(),
352+
});
321353
}
322354
}
323355
"region_start" => {
324-
pending_comments.push(text);
356+
pending_comments.push(PendingAttachment {
357+
start_byte: node.start_byte(),
358+
text: text.clone(),
359+
});
325360
}
326361
"region_end" => {
327362
region_end_comment = Some(text.clone());
@@ -340,22 +375,36 @@ fn extract_tokens_to_reorder(
340375
});
341376
}
342377
_ => {
343-
pending_annotations.push(text);
378+
pending_annotations.push(PendingAttachment {
379+
start_byte: node.start_byte(),
380+
text: text.clone(),
381+
});
344382
}
345383
}
346384
} else {
347-
pending_annotations.push(text);
385+
pending_annotations.push(PendingAttachment {
386+
start_byte: node.start_byte(),
387+
text: text.clone(),
388+
});
348389
}
349390
}
350391
"class_name_statement" => {
351392
if let Some(element) = reorderable_element {
352393
// Don't attach class docstring to class_name, save it for extends
353-
let mut non_docstring_comments = Vec::new();
354-
for comment in &pending_comments {
355-
if !class_docstring_comments.contains(comment) {
356-
non_docstring_comments.push(comment.clone());
357-
}
358-
}
394+
let mut attachments: Vec<&PendingAttachment> =
395+
pending_annotations.iter().collect();
396+
attachments.extend(pending_comments.iter());
397+
attachments.sort_by_key(|attachment| attachment.start_byte);
398+
let non_docstring_comments: Vec<String> = attachments
399+
.into_iter()
400+
.filter_map(|attachment| {
401+
if !class_docstring_comments.contains(&attachment.text) {
402+
Some(attachment.text.clone())
403+
} else {
404+
None
405+
}
406+
})
407+
.collect();
359408
elements.push(GDScriptTokensWithComments {
360409
token_kind: element,
361410
attached_comments: non_docstring_comments,
@@ -371,9 +420,11 @@ fn extract_tokens_to_reorder(
371420
"extends_statement" => {
372421
found_extends_declaration = true;
373422
if let Some(element) = reorderable_element {
423+
let combined_comments =
424+
merge_pending_texts(&pending_annotations, &pending_comments);
374425
elements.push(GDScriptTokensWithComments {
375426
token_kind: element,
376-
attached_comments: pending_comments.clone(),
427+
attached_comments: combined_comments,
377428
trailing_comments: Vec::new(),
378429
original_text: text,
379430
start_byte: node.start_byte(),
@@ -417,8 +468,8 @@ fn extract_tokens_to_reorder(
417468
class_docstring_attached = true;
418469
}
419470

420-
let mut combined_comments = pending_annotations.clone();
421-
combined_comments.extend(pending_comments.clone());
471+
let combined_comments =
472+
merge_pending_texts(&pending_annotations, &pending_comments);
422473

423474
// We store trailing #endregion comments to attach them to
424475
// the most recent function that has a #region comment at
@@ -454,9 +505,11 @@ fn extract_tokens_to_reorder(
454505
// We create unknown element for unhandled nodes to preserve
455506
// them. Given how the module works, if we don't do that the
456507
// nodes will be dropped.
508+
let combined_comments =
509+
merge_pending_texts(&pending_annotations, &pending_comments);
457510
elements.push(GDScriptTokensWithComments {
458511
token_kind: GDScriptTokenKind::Unknown(text.clone()),
459-
attached_comments: pending_comments.clone(),
512+
attached_comments: combined_comments,
460513
trailing_comments: Vec::new(),
461514
original_text: text,
462515
start_byte: node.start_byte(),
@@ -469,6 +522,58 @@ fn extract_tokens_to_reorder(
469522
}
470523
}
471524

525+
// `region_end_comment` stores a trailing `#endregion` that should follow
526+
// the most recent function with a matching `#region`. If we found no
527+
// matching function, we fall back to attaching it to the last element (or
528+
// create a standalone element at the end). This avoids the formatter
529+
// deleting or losing the directive when we reorder code blocks.
530+
if let Some(region_end) = region_end_comment.take() {
531+
if let Some(target_element) = elements.iter_mut().rev().find(|element| {
532+
matches!(element.token_kind, GDScriptTokenKind::Method(_, _, _))
533+
&& element
534+
.attached_comments
535+
.iter()
536+
.any(|c| c.trim().starts_with("#region"))
537+
}) {
538+
target_element.trailing_comments.push(region_end);
539+
} else if let Some(last_element) = elements.last_mut() {
540+
last_element.trailing_comments.push(region_end);
541+
} else {
542+
elements.push(GDScriptTokensWithComments {
543+
token_kind: GDScriptTokenKind::Unknown(region_end.clone()),
544+
attached_comments: Vec::new(),
545+
trailing_comments: Vec::new(),
546+
original_text: region_end,
547+
start_byte: 0,
548+
end_byte: 0,
549+
});
550+
}
551+
}
552+
553+
// Comments and annotations that we have not yet attached go at the end of
554+
// the file. They are either trailing comments, stray `#region` markers, or
555+
// unknown statements that appear after the last declaration (like new
556+
// syntax in a dev version of Godot). We merge them in source order with
557+
// `merge_pending_texts` and append them either to the last reordered
558+
// element or to a dedicated "Unknown" token so the text remains in the
559+
// output.
560+
if !pending_annotations.is_empty() || !pending_comments.is_empty() {
561+
let trailing_texts = merge_pending_texts(&pending_annotations, &pending_comments);
562+
if let Some(last_element) = elements.last_mut() {
563+
last_element.trailing_comments.extend(trailing_texts);
564+
} else if !trailing_texts.is_empty() {
565+
let combined_text = trailing_texts.join("\n");
566+
elements.push(GDScriptTokensWithComments {
567+
token_kind: GDScriptTokenKind::Unknown(combined_text.clone()),
568+
attached_comments: Vec::new(),
569+
trailing_comments: Vec::new(),
570+
original_text: combined_text,
571+
start_byte: 0,
572+
end_byte: 0,
573+
});
574+
}
575+
}
576+
472577
Ok(elements)
473578
}
474579

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@tool
2+
extends Node
3+
4+
#@export var speed_air_cap: float = 1
5+
@export_subgroup("Dodge")
6+
@export var speed_dodge: float = 15
7+
@export var speed_run: float = 20
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
extends Node
2+
3+
4+
func first_function() -> void:
5+
print("Hello")
6+
# func second_function() -> void:
7+
# print("World!")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@tool
2+
extends Node
3+
4+
#@export var speed_air_cap: float = 1
5+
@export_subgroup("Dodge")
6+
@export var speed_dodge: float = 15
7+
8+
@export var speed_run: float = 20
9+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
extends Node
2+
3+
func first_function() -> void:
4+
print("Hello")
5+
6+
# func second_function() -> void:
7+
# print("World!")
8+

0 commit comments

Comments
 (0)