1use php_ast::{
2 CallableCreateKind, ClassDecl, ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody,
3 Param, PropertyHookBody, Stmt, StmtKind, StringPart,
4};
5use tower_lsp::lsp_types::{Position, Range, SelectionRange};
6
7use crate::ast::{ParsedDoc, SourceView};
8
9pub fn selection_ranges(doc: &ParsedDoc, positions: &[Position]) -> Vec<SelectionRange> {
12 let sv = doc.view();
13 let fr = file_range(sv);
14 positions
15 .iter()
16 .map(|pos| {
17 let byte_off = sv.byte_of_position(*pos);
18 build_chain(sv, &doc.program().stmts, byte_off, fr)
19 })
20 .collect()
21}
22
23fn file_range(sv: SourceView<'_>) -> Range {
29 let source = sv.source();
30 let line_starts = sv.line_starts();
31 if source.is_empty() {
32 return Range {
33 start: Position {
34 line: 0,
35 character: 0,
36 },
37 end: Position {
38 line: 0,
39 character: 0,
40 },
41 };
42 }
43 let last_line_idx = line_starts.len().saturating_sub(1) as u32;
44 let last_line_start = *line_starts.last().unwrap_or(&0) as usize;
45 let raw = &source[last_line_start..];
46 let line = raw.strip_suffix('\n').unwrap_or(raw);
47 let line = line.strip_suffix('\r').unwrap_or(line);
48 let last_char: u32 = line.chars().map(|c| c.len_utf16() as u32).sum();
49 Range {
50 start: Position {
51 line: 0,
52 character: 0,
53 },
54 end: Position {
55 line: last_line_idx,
56 character: last_char,
57 },
58 }
59}
60
61fn build_chain(
63 sv: SourceView<'_>,
64 stmts: &[Stmt<'_, '_>],
65 byte_off: u32,
66 fr: Range,
67) -> SelectionRange {
68 let mut spans: Vec<(u32, u32)> = Vec::new();
69 collect_spans_stmts(stmts, byte_off, &mut spans);
70 spans.sort_by_key(|&(s, e)| e - s);
77 spans.dedup();
78 let ranges: Vec<Range> = spans
79 .into_iter()
80 .map(|(s, e)| span_range(sv, s, e))
81 .collect();
82 let mut ranges = ranges;
83 ranges.dedup();
84
85 if !ranges.contains(&fr) {
87 ranges.push(fr);
88 }
89
90 let mut chain: Option<SelectionRange> = None;
92 for range in ranges.into_iter().rev() {
93 chain = Some(SelectionRange {
94 range,
95 parent: chain.map(Box::new),
96 });
97 }
98
99 chain.unwrap_or(SelectionRange {
100 range: fr,
101 parent: None,
102 })
103}
104
105#[cfg(test)]
106fn contains(range: Range, pos: Position) -> bool {
107 if pos.line < range.start.line || pos.line > range.end.line {
108 return false;
109 }
110 if pos.line == range.start.line && pos.character < range.start.character {
111 return false;
112 }
113 if pos.line == range.end.line && pos.character >= range.end.character {
114 return false;
115 }
116 true
117}
118
119fn span_range(sv: SourceView<'_>, start: u32, end: u32) -> Range {
120 Range {
121 start: sv.position_of(start),
122 end: sv.position_of(end),
123 }
124}
125
126#[inline]
127fn span_contains(start: u32, end: u32, off: u32) -> bool {
128 off >= start && off < end
129}
130
131#[inline]
132fn push_if_contains(s: u32, e: u32, off: u32, out: &mut Vec<(u32, u32)>) -> bool {
133 if span_contains(s, e, off) {
134 out.push((s, e));
135 true
136 } else {
137 false
138 }
139}
140
141fn collect_spans_stmts(stmts: &[Stmt<'_, '_>], off: u32, out: &mut Vec<(u32, u32)>) {
142 for stmt in stmts {
143 collect_spans_stmt(stmt, off, out);
144 }
145}
146
147fn collect_spans_stmt(stmt: &Stmt<'_, '_>, off: u32, out: &mut Vec<(u32, u32)>) {
148 let s = stmt.span.start;
149 let e = stmt.span.end;
150 if !span_contains(s, e, off) {
151 return;
152 }
153 out.push((s, e));
154 match &stmt.kind {
155 StmtKind::Function(f) => {
156 for p in f.params.iter() {
157 collect_spans_param(p, off, out);
158 }
159 collect_spans_stmts(&f.body.stmts, off, out);
160 }
161 StmtKind::Class(c) => collect_class_members(c, off, out),
162 StmtKind::Interface(i) => {
163 for member in i.body.members.iter() {
164 collect_class_member(member, off, out);
165 }
166 }
167 StmtKind::Trait(t) => {
168 for member in t.body.members.iter() {
169 collect_class_member(member, off, out);
170 }
171 }
172 StmtKind::Enum(en) => {
173 for member in en.body.members.iter() {
174 if !push_if_contains(member.span.start, member.span.end, off, out) {
175 continue;
176 }
177 match &member.kind {
178 EnumMemberKind::Method(m) => {
179 for p in m.params.iter() {
180 collect_spans_param(p, off, out);
181 }
182 if let Some(body) = &m.body {
183 collect_spans_stmts(&body.stmts, off, out);
184 }
185 }
186 EnumMemberKind::Case(c) => {
187 if let Some(v) = &c.value {
188 collect_spans_expr(v, off, out);
189 }
190 }
191 EnumMemberKind::ClassConst(c) => {
192 collect_spans_expr(&c.value, off, out);
193 }
194 EnumMemberKind::TraitUse(_) => {}
195 }
196 }
197 }
198 StmtKind::Namespace(ns) => {
199 if let NamespaceBody::Braced(inner) = &ns.body {
200 collect_spans_stmts(&inner.stmts, off, out);
201 }
202 }
203 StmtKind::If(i) => {
204 collect_spans_expr(&i.condition, off, out);
205 collect_spans_stmt(i.then_branch, off, out);
206 for ei in i.elseif_branches.iter() {
207 if !push_if_contains(ei.span.start, ei.span.end, off, out) {
208 continue;
209 }
210 collect_spans_expr(&ei.condition, off, out);
211 collect_spans_stmt(&ei.body, off, out);
212 }
213 if let Some(el) = &i.else_branch {
214 collect_spans_stmt(el, off, out);
215 }
216 }
217 StmtKind::While(w) => {
218 collect_spans_expr(&w.condition, off, out);
219 collect_spans_stmt(w.body, off, out);
220 }
221 StmtKind::For(f) => {
222 for e in f.init.iter() {
223 collect_spans_expr(e, off, out);
224 }
225 for e in f.condition.iter() {
226 collect_spans_expr(e, off, out);
227 }
228 for e in f.update.iter() {
229 collect_spans_expr(e, off, out);
230 }
231 collect_spans_stmt(f.body, off, out);
232 }
233 StmtKind::Foreach(f) => {
234 collect_spans_expr(&f.expr, off, out);
235 if let Some(k) = &f.key {
236 collect_spans_expr(k, off, out);
237 }
238 collect_spans_expr(&f.value, off, out);
239 collect_spans_stmt(f.body, off, out);
240 }
241 StmtKind::DoWhile(d) => {
242 collect_spans_stmt(d.body, off, out);
243 collect_spans_expr(&d.condition, off, out);
244 }
245 StmtKind::Switch(sw) => {
246 collect_spans_expr(&sw.expr, off, out);
247 for case in sw.body.cases.iter() {
248 if !push_if_contains(case.span.start, case.span.end, off, out) {
249 continue;
250 }
251 if let Some(v) = &case.value {
252 collect_spans_expr(v, off, out);
253 }
254 collect_spans_stmts(&case.body, off, out);
255 }
256 }
257 StmtKind::TryCatch(t) => {
258 collect_spans_stmts(&t.body.stmts, off, out);
259 for catch in t.catches.iter() {
260 if !push_if_contains(catch.span.start, catch.span.end, off, out) {
261 continue;
262 }
263 collect_spans_stmts(&catch.body.stmts, off, out);
264 }
265 if let Some(finally) = &t.finally {
266 collect_spans_stmts(&finally.stmts, off, out);
267 }
268 }
269 StmtKind::Block(stmts) => collect_spans_stmts(&stmts.stmts, off, out),
270 StmtKind::Expression(e) => collect_spans_expr(e, off, out),
271 StmtKind::Echo(args) => {
272 for a in args.iter() {
273 collect_spans_expr(a, off, out);
274 }
275 }
276 StmtKind::Return(opt) => {
277 if let Some(e) = opt {
278 collect_spans_expr(e, off, out);
279 }
280 }
281 StmtKind::Break(opt) | StmtKind::Continue(opt) => {
282 if let Some(e) = opt {
283 collect_spans_expr(e, off, out);
284 }
285 }
286 StmtKind::Throw(e) => collect_spans_expr(e, off, out),
287 StmtKind::Unset(args) => {
288 for a in args.iter() {
289 collect_spans_expr(a, off, out);
290 }
291 }
292 StmtKind::Const(items) => {
293 for item in items.iter() {
294 collect_spans_expr(&item.value, off, out);
295 }
296 }
297 StmtKind::StaticVar(items) => {
298 for item in items.iter() {
299 if let Some(d) = &item.default {
300 collect_spans_expr(d, off, out);
301 }
302 }
303 }
304 StmtKind::Declare(d) => {
305 for (_, e) in d.directives.iter() {
306 collect_spans_expr(e, off, out);
307 }
308 if let Some(body) = &d.body {
309 collect_spans_stmt(body, off, out);
310 }
311 }
312 StmtKind::Use(_)
315 | StmtKind::Global(_)
316 | StmtKind::Goto(_)
317 | StmtKind::Label(_)
318 | StmtKind::HaltCompiler(_)
319 | StmtKind::Nop
320 | StmtKind::InlineHtml(_)
321 | StmtKind::Error => {}
322 }
323}
324
325fn collect_class_members(c: &ClassDecl<'_, '_>, off: u32, out: &mut Vec<(u32, u32)>) {
326 for member in c.body.members.iter() {
327 collect_class_member(member, off, out);
328 }
329}
330
331fn collect_class_member(
332 member: &php_ast::ClassMember<'_, '_>,
333 off: u32,
334 out: &mut Vec<(u32, u32)>,
335) {
336 if !push_if_contains(member.span.start, member.span.end, off, out) {
337 return;
338 }
339 match &member.kind {
340 ClassMemberKind::Method(m) => {
341 for p in m.params.iter() {
342 collect_spans_param(p, off, out);
343 }
344 if let Some(body) = &m.body {
345 collect_spans_stmts(&body.stmts, off, out);
346 }
347 }
348 ClassMemberKind::Property(p) => {
349 if let Some(d) = &p.default {
350 collect_spans_expr(d, off, out);
351 }
352 for hook in p.hooks.iter() {
353 if !push_if_contains(hook.span.start, hook.span.end, off, out) {
354 continue;
355 }
356 for hp in hook.params.iter() {
357 collect_spans_param(hp, off, out);
358 }
359 match &hook.body {
360 PropertyHookBody::Block(stmts) => collect_spans_stmts(&stmts.stmts, off, out),
361 PropertyHookBody::Expression(e) => collect_spans_expr(e, off, out),
362 PropertyHookBody::Abstract => {}
363 }
364 }
365 }
366 ClassMemberKind::ClassConst(c) => collect_spans_expr(&c.value, off, out),
367 ClassMemberKind::TraitUse(_) => {}
368 }
369}
370
371fn collect_spans_param(p: &Param<'_, '_>, off: u32, out: &mut Vec<(u32, u32)>) {
372 if !push_if_contains(p.span.start, p.span.end, off, out) {
373 return;
374 }
375 if let Some(d) = &p.default {
376 collect_spans_expr(d, off, out);
377 }
378 for hook in p.hooks.iter() {
379 if !push_if_contains(hook.span.start, hook.span.end, off, out) {
380 continue;
381 }
382 match &hook.body {
383 PropertyHookBody::Block(stmts) => collect_spans_stmts(&stmts.stmts, off, out),
384 PropertyHookBody::Expression(e) => collect_spans_expr(e, off, out),
385 PropertyHookBody::Abstract => {}
386 }
387 }
388}
389
390fn collect_spans_expr(expr: &Expr<'_, '_>, off: u32, out: &mut Vec<(u32, u32)>) {
391 let s = expr.span.start;
392 let e = expr.span.end;
393 if !span_contains(s, e, off) {
394 return;
395 }
396 out.push((s, e));
397 match &expr.kind {
398 ExprKind::Int(_)
400 | ExprKind::Float(_)
401 | ExprKind::String(_)
402 | ExprKind::Bool(_)
403 | ExprKind::Null
404 | ExprKind::Variable(_)
405 | ExprKind::Identifier(_)
406 | ExprKind::MagicConst(_)
407 | ExprKind::Nowdoc { .. }
408 | ExprKind::Error => {}
409
410 ExprKind::InterpolatedString(parts) | ExprKind::ShellExec(parts) => {
411 for p in parts.iter() {
412 if let StringPart::Expr(inner) = p {
413 collect_spans_expr(inner, off, out);
414 }
415 }
416 }
417 ExprKind::Heredoc { parts, .. } => {
418 for p in parts.iter() {
419 if let StringPart::Expr(inner) = p {
420 collect_spans_expr(inner, off, out);
421 }
422 }
423 }
424
425 ExprKind::VariableVariable(inner) => collect_spans_expr(inner, off, out),
426 ExprKind::Assign(a) => {
427 collect_spans_expr(a.target, off, out);
428 collect_spans_expr(a.value, off, out);
429 }
430 ExprKind::Binary(b) => {
431 collect_spans_expr(b.left, off, out);
432 collect_spans_expr(b.right, off, out);
433 }
434 ExprKind::UnaryPrefix(u) => collect_spans_expr(u.operand, off, out),
435 ExprKind::UnaryPostfix(u) => collect_spans_expr(u.operand, off, out),
436 ExprKind::Ternary(t) => {
437 collect_spans_expr(t.condition, off, out);
438 if let Some(then_e) = t.then_expr {
439 collect_spans_expr(then_e, off, out);
440 }
441 collect_spans_expr(t.else_expr, off, out);
442 }
443 ExprKind::NullCoalesce(n) => {
444 collect_spans_expr(n.left, off, out);
445 collect_spans_expr(n.right, off, out);
446 }
447 ExprKind::FunctionCall(f) => {
448 collect_spans_expr(f.name, off, out);
449 for arg in f.args.iter() {
450 if !push_if_contains(arg.span.start, arg.span.end, off, out) {
451 continue;
452 }
453 collect_spans_expr(&arg.value, off, out);
454 }
455 }
456 ExprKind::Array(elems) => {
457 for el in elems.iter() {
458 if !push_if_contains(el.span.start, el.span.end, off, out) {
459 continue;
460 }
461 if let Some(k) = &el.key {
462 collect_spans_expr(k, off, out);
463 }
464 collect_spans_expr(&el.value, off, out);
465 }
466 }
467 ExprKind::ArrayAccess(a) => {
468 collect_spans_expr(a.array, off, out);
469 if let Some(idx) = a.index {
470 collect_spans_expr(idx, off, out);
471 }
472 }
473 ExprKind::Print(e) => collect_spans_expr(e, off, out),
474 ExprKind::Parenthesized(e) => collect_spans_expr(e, off, out),
475 ExprKind::Cast(_, e) => collect_spans_expr(e, off, out),
476 ExprKind::ErrorSuppress(e) => collect_spans_expr(e, off, out),
477 ExprKind::Isset(es) => {
478 for e in es.iter() {
479 collect_spans_expr(e, off, out);
480 }
481 }
482 ExprKind::Empty(e) => collect_spans_expr(e, off, out),
483 ExprKind::Include(_, e) => collect_spans_expr(e, off, out),
484 ExprKind::Eval(e) => collect_spans_expr(e, off, out),
485 ExprKind::Exit(opt) => {
486 if let Some(e) = opt {
487 collect_spans_expr(e, off, out);
488 }
489 }
490 ExprKind::Clone(e) => collect_spans_expr(e, off, out),
491 ExprKind::New(n) => {
492 collect_spans_expr(n.class, off, out);
493 for arg in n.args.iter() {
494 if !push_if_contains(arg.span.start, arg.span.end, off, out) {
495 continue;
496 }
497 collect_spans_expr(&arg.value, off, out);
498 }
499 }
500 ExprKind::PropertyAccess(p) | ExprKind::NullsafePropertyAccess(p) => {
501 collect_spans_expr(p.object, off, out);
502 collect_spans_expr(p.property, off, out);
503 }
504 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
505 collect_spans_expr(m.object, off, out);
506 collect_spans_expr(m.method, off, out);
507 for arg in m.args.iter() {
508 if !push_if_contains(arg.span.start, arg.span.end, off, out) {
509 continue;
510 }
511 collect_spans_expr(&arg.value, off, out);
512 }
513 }
514 ExprKind::StaticPropertyAccess(s) | ExprKind::ClassConstAccess(s) => {
515 collect_spans_expr(s.class, off, out);
516 }
517 ExprKind::StaticMethodCall(s) => {
518 collect_spans_expr(s.class, off, out);
519 for arg in s.args.iter() {
520 if !push_if_contains(arg.span.start, arg.span.end, off, out) {
521 continue;
522 }
523 collect_spans_expr(&arg.value, off, out);
524 }
525 }
526 ExprKind::ClassConstAccessDynamic { class, member }
527 | ExprKind::StaticPropertyAccessDynamic { class, member } => {
528 collect_spans_expr(class, off, out);
529 collect_spans_expr(member, off, out);
530 }
531 ExprKind::Closure(c) => {
532 for p in c.params.iter() {
533 collect_spans_param(p, off, out);
534 }
535 collect_spans_stmts(&c.body.stmts, off, out);
536 }
537 ExprKind::ArrowFunction(a) => {
538 for p in a.params.iter() {
539 collect_spans_param(p, off, out);
540 }
541 collect_spans_expr(a.body, off, out);
542 }
543 ExprKind::Match(m) => {
544 collect_spans_expr(m.subject, off, out);
545 for arm in m.arms.iter() {
546 if !push_if_contains(arm.span.start, arm.span.end, off, out) {
547 continue;
548 }
549 if let Some(conds) = &arm.conditions {
550 for c in conds.iter() {
551 collect_spans_expr(c, off, out);
552 }
553 }
554 collect_spans_expr(&arm.body, off, out);
555 }
556 }
557 ExprKind::ThrowExpr(e) => collect_spans_expr(e, off, out),
558 ExprKind::Yield(y) => {
559 if let Some(k) = y.key {
560 collect_spans_expr(k, off, out);
561 }
562 if let Some(v) = y.value {
563 collect_spans_expr(v, off, out);
564 }
565 }
566 ExprKind::AnonymousClass(c) => collect_class_members(c, off, out),
567 ExprKind::CallableCreate(c) => match &c.kind {
568 CallableCreateKind::Function(e) => collect_spans_expr(e, off, out),
569 CallableCreateKind::Method { object, .. } => collect_spans_expr(object, off, out),
570 CallableCreateKind::NullsafeMethod { object, .. } => {
571 collect_spans_expr(object, off, out)
572 }
573 CallableCreateKind::StaticMethod { class, .. } => collect_spans_expr(class, off, out),
574 },
575 ExprKind::CloneWith(target, withs) => {
576 collect_spans_expr(target, off, out);
577 collect_spans_expr(withs, off, out);
578 }
579 ExprKind::StaticDynMethodCall(s) => {
580 collect_spans_expr(s.class, off, out);
581 collect_spans_expr(s.method, off, out);
582 for arg in s.args.iter() {
583 if !push_if_contains(arg.span.start, arg.span.end, off, out) {
584 continue;
585 }
586 collect_spans_expr(&arg.value, off, out);
587 }
588 }
589 ExprKind::Omit => {}
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 fn doc(src: &str) -> ParsedDoc {
598 ParsedDoc::parse(src.to_string())
599 }
600
601 fn pos(line: u32, character: u32) -> Position {
602 Position { line, character }
603 }
604
605 fn chain_ranges(sr: &SelectionRange) -> Vec<Range> {
606 let mut ranges = vec![sr.range];
607 let mut current = sr.parent.as_deref();
608 while let Some(p) = current {
609 ranges.push(p.range);
610 current = p.parent.as_deref();
611 }
612 ranges
613 }
614
615 #[test]
616 fn returns_one_result_per_position() {
617 let src = "<?php\nfunction greet() {}";
618 let d = doc(src);
619 let positions = vec![pos(1, 10), pos(0, 0)];
620 let result = selection_ranges(&d, &positions);
621 assert_eq!(result.len(), 2);
622 }
623
624 #[test]
625 fn empty_file_returns_file_range() {
626 let src = "<?php";
627 let d = doc(src);
628 let result = selection_ranges(&d, &[pos(0, 0)]);
629 assert_eq!(result.len(), 1);
630 assert_eq!(result[0].range.start.line, 0);
631 }
632
633 #[test]
634 fn cursor_in_function_body_includes_function_range() {
635 let src = "<?php\nfunction greet() {\n echo 'hi';\n}";
636 let d = doc(src);
637 let result = selection_ranges(&d, &[pos(2, 4)]);
638 let ranges = chain_ranges(&result[0]);
639 assert!(
640 ranges.iter().any(|r| r.start.line == 1),
641 "expected a range starting at line 1 (function), got {:?}",
642 ranges
643 );
644 }
645
646 #[test]
647 fn cursor_in_method_body_includes_method_and_class_ranges() {
648 let src = "<?php\nclass Foo {\n public function bar() {\n echo 1;\n }\n}";
649 let d = doc(src);
650 let result = selection_ranges(&d, &[pos(3, 8)]);
651 let ranges = chain_ranges(&result[0]);
652 assert!(
653 ranges.iter().any(|r| r.start.line == 1),
654 "expected class-level range at line 1, got {:?}",
655 ranges
656 );
657 assert!(
658 ranges.iter().any(|r| r.start.line == 2),
659 "expected method-level range at line 2, got {:?}",
660 ranges
661 );
662 }
663
664 #[test]
665 fn cursor_outside_all_nodes_returns_file_range_only() {
666 let src = "<?php\n// comment\n";
667 let d = doc(src);
668 let result = selection_ranges(&d, &[pos(1, 0)]);
669 assert!(!result.is_empty());
670 assert_eq!(result[0].range.start.line, 0);
671 }
672
673 #[test]
674 fn chain_is_ordered_innermost_to_outermost() {
675 let src = "<?php\nclass Foo {\n public function bar() {\n echo 1;\n }\n}";
676 let d = doc(src);
677 let result = selection_ranges(&d, &[pos(3, 8)]);
678 let ranges = chain_ranges(&result[0]);
679 for window in ranges.windows(2) {
680 let inner = &window[0];
681 let outer = &window[1];
682 let inner_lines = inner.end.line - inner.start.line;
683 let outer_lines = outer.end.line - outer.start.line;
684 assert!(
685 outer_lines >= inner_lines,
686 "outer range should be >= inner range: inner={:?}, outer={:?}",
687 inner,
688 outer
689 );
690 }
691 }
692
693 #[test]
694 fn multiple_positions_are_independent() {
695 let src = "<?php\nfunction a() {}\nfunction b() {}";
696 let d = doc(src);
697 let result = selection_ranges(&d, &[pos(1, 10), pos(2, 10)]);
698 assert_eq!(result.len(), 2);
699 assert_ne!(result[0].range, result[1].range);
700 }
701
702 #[test]
705 fn contains_excludes_exact_end_position() {
706 let range = Range {
710 start: Position {
711 line: 0,
712 character: 4,
713 },
714 end: Position {
715 line: 0,
716 character: 9,
717 },
718 };
719 assert!(
720 !contains(
721 range,
722 Position {
723 line: 0,
724 character: 9
725 }
726 ),
727 "exact end position must be outside (half-open range)"
728 );
729 assert!(
730 !contains(
731 range,
732 Position {
733 line: 0,
734 character: 10
735 }
736 ),
737 "position after end must be outside"
738 );
739 assert!(
740 contains(
741 range,
742 Position {
743 line: 0,
744 character: 8
745 }
746 ),
747 "position just before end must be inside"
748 );
749 assert!(
750 contains(
751 range,
752 Position {
753 line: 0,
754 character: 4
755 }
756 ),
757 "start position must be inside"
758 );
759 }
760
761 #[test]
762 fn contains_handles_multiline_range_end() {
763 let range = Range {
764 start: Position {
765 line: 1,
766 character: 0,
767 },
768 end: Position {
769 line: 3,
770 character: 1,
771 },
772 };
773 assert!(!contains(
775 range,
776 Position {
777 line: 3,
778 character: 1
779 }
780 ));
781 assert!(contains(
783 range,
784 Position {
785 line: 3,
786 character: 0
787 }
788 ));
789 assert!(contains(
791 range,
792 Position {
793 line: 2,
794 character: 999
795 }
796 ));
797 }
798
799 #[test]
800 fn file_range_end_character_is_actual_line_length_not_u32_max() {
801 let src = "<?php\nfunction hello(): void {}";
805 let d = doc(src);
807 let result = selection_ranges(&d, &[pos(1, 10)]);
808 let ranges = chain_ranges(&result[0]);
809 let outermost = ranges.last().expect("should have at least one range");
810 assert_ne!(
811 outermost.end.character,
812 u32::MAX,
813 "end character must not be u32::MAX — use real line length"
814 );
815 assert_eq!(
817 outermost.end.character, 25,
818 "file-level end character should be the actual last-line length"
819 );
820 }
821}