php_lsp/actions/
phpdoc_action.rs1use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Param, Stmt, StmtKind};
3use tower_lsp::lsp_types::{
4 CodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit, Url, WorkspaceEdit,
5};
6
7use crate::document::ast::{ParsedDoc, SourceView, format_type_hint};
8
9pub fn phpdoc_actions(
12 uri: &Url,
13 doc: &ParsedDoc,
14 _source: &str,
15 range: Range,
16) -> Vec<CodeActionOrCommand> {
17 let sv = doc.view();
18 let mut actions = Vec::new();
19 collect(&doc.program().stmts, uri, sv, range, &mut actions);
20 actions
21}
22
23fn collect(
24 stmts: &[Stmt<'_, '_>],
25 uri: &Url,
26 sv: SourceView<'_>,
27 range: Range,
28 out: &mut Vec<CodeActionOrCommand>,
29) {
30 for stmt in stmts {
31 match &stmt.kind {
32 StmtKind::Function(f) => {
33 let fn_line = sv.position_of(stmt.span.start).line;
34 if line_in_range(fn_line, range) && f.doc_comment.is_none() {
35 let ret = f.return_type.as_ref().map(|t| format_type_hint(t));
36 if let Some(action) = make_action(uri, sv.source(), fn_line, &f.params, ret) {
37 out.push(action);
38 }
39 }
40 }
41 StmtKind::Class(c) => {
42 for member in c.body.members.iter() {
43 if let ClassMemberKind::Method(m) = &member.kind {
44 let fn_line = sv.position_of(member.span.start).line;
45 if line_in_range(fn_line, range) && m.doc_comment.is_none() {
46 let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
47 if let Some(action) =
48 make_action(uri, sv.source(), fn_line, &m.params, ret)
49 {
50 out.push(action);
51 }
52 }
53 }
54 }
55 }
56 StmtKind::Trait(t) => {
57 for member in t.body.members.iter() {
58 if let ClassMemberKind::Method(m) = &member.kind {
59 let fn_line = sv.position_of(member.span.start).line;
60 if line_in_range(fn_line, range) && m.doc_comment.is_none() {
61 let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
62 if let Some(action) =
63 make_action(uri, sv.source(), fn_line, &m.params, ret)
64 {
65 out.push(action);
66 }
67 }
68 }
69 }
70 }
71 StmtKind::Enum(e) => {
72 for member in e.body.members.iter() {
73 if let EnumMemberKind::Method(m) = &member.kind {
74 let fn_line = sv.position_of(member.span.start).line;
75 if line_in_range(fn_line, range) && m.doc_comment.is_none() {
76 let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
77 if let Some(action) =
78 make_action(uri, sv.source(), fn_line, &m.params, ret)
79 {
80 out.push(action);
81 }
82 }
83 }
84 }
85 }
86 StmtKind::Namespace(ns) => {
87 if let NamespaceBody::Braced(inner) = &ns.body {
88 collect(&inner.stmts, uri, sv, range, out);
89 }
90 }
91 _ => {}
92 }
93 }
94}
95
96fn line_in_range(line: u32, range: Range) -> bool {
97 line >= range.start.line && line <= range.end.line
98}
99
100fn make_action(
101 uri: &Url,
102 source: &str,
103 fn_line: u32,
104 params: &[Param<'_, '_>],
105 return_type: Option<String>,
106) -> Option<CodeActionOrCommand> {
107 let indent = source
108 .lines()
109 .nth(fn_line as usize)
110 .map(|line| {
111 let n = line.len() - line.trim_start().len();
112 &line[..n]
113 })
114 .unwrap_or("")
115 .to_string();
116
117 let mut lines: Vec<String> = vec![format!("{indent}/**")];
118
119 for p in params.iter() {
120 let type_part = p
121 .type_hint
122 .as_ref()
123 .map(|t| format!("{} ", format_type_hint(t)))
124 .unwrap_or_default();
125 let name = format!("${}", p.name);
126 lines.push(format!("{indent} * @param {type_part}{name}"));
127 }
128
129 if let Some(ret) = return_type {
130 lines.push(format!("{indent} * @return {ret}"));
131 }
132
133 lines.push(format!("{indent} */"));
134
135 let new_text = lines.join("\n") + "\n";
136 let pos = Position {
137 line: fn_line,
138 character: 0,
139 };
140 let edit = TextEdit {
141 range: Range {
142 start: pos,
143 end: pos,
144 },
145 new_text,
146 };
147
148 let mut changes = std::collections::HashMap::new();
149 changes.insert(uri.clone(), vec![edit]);
150
151 Some(CodeActionOrCommand::CodeAction(CodeAction {
152 title: "Generate PHPDoc".to_string(),
153 kind: Some(CodeActionKind::REFACTOR),
154 edit: Some(WorkspaceEdit {
155 changes: Some(changes),
156 ..Default::default()
157 }),
158 ..Default::default()
159 }))
160}