Skip to main content

php_lsp/editing/
selection_range.rs

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
9/// Build a selection-range chain for each cursor position.
10/// Levels go from innermost to outermost via `parent` links.
11pub 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
23/// The entire file as a single range.
24///
25/// Uses the precomputed `line_starts` table to jump to the last line rather
26/// than doing an O(file_size) `source.lines().collect()`. Only scans the last
27/// line's characters to compute the UTF-16 end column.
28fn 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
61/// Build the innermost-to-outermost chain for a cursor position.
62fn 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    // Sort by byte width ascending so the innermost (smallest) span is
71    // first. Working in byte space — instead of mapping to LSP ranges
72    // first — keeps two same-line-span ranges (e.g. a `while` statement
73    // and its inner body block, both ending at the same `}`) correctly
74    // ordered: parent ranges always end up *outer* than their children
75    // even when the column-based key would tie.
76    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    // Ensure file-level range is outermost
86    if !ranges.contains(&fr) {
87        ranges.push(fr);
88    }
89
90    // Build linked chain from outermost inward
91    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        // Variants whose payload is a name list, raw text, or empty: nothing
313        // useful to add beyond the statement span we already pushed.
314        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        // Atoms — no children.
399        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    // ── contains() boundary regression tests ─────────────────────────────────
703
704    #[test]
705    fn contains_excludes_exact_end_position() {
706        // LSP ranges are half-open [start, end).  The old code used `>` instead
707        // of `>=` for the end-character check, so a position exactly at
708        // range.end was incorrectly treated as inside the range.
709        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        // On the end line, character == end.character is outside.
774        assert!(!contains(
775            range,
776            Position {
777                line: 3,
778                character: 1
779            }
780        ));
781        // On the end line, character < end.character is inside.
782        assert!(contains(
783            range,
784            Position {
785                line: 3,
786                character: 0
787            }
788        ));
789        // Line between start and end — always inside regardless of character.
790        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        // The outermost range must use the real UTF-16 column length of the last
802        // line, not u32::MAX.  u32::MAX is not LSP-spec-compliant and causes
803        // issues with stricter clients.
804        let src = "<?php\nfunction hello(): void {}";
805        //         line 0             line 1 (30 chars)
806        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        // "function hello(): void {}" is 25 chars; the file-level range should end there.
816        assert_eq!(
817            outermost.end.character, 25,
818            "file-level end character should be the actual last-line length"
819        );
820    }
821}