1use strop_picker::{IndentChoice, Item, Kind, Payload, Picker};
7
8use super::document::IndentSource;
9use super::Editor;
10use crate::config::IndentStyle;
11
12impl Editor {
13 pub fn indentation_at(
15 &self,
16 document: strop_core::id::DocumentId,
17 byte: usize,
18 ) -> super::document::Indent {
19 let source = self.indent_target_at(document, byte).unwrap_or(document);
20 self.doc(source).indent
21 }
22
23 pub fn tab_width_for_location(&self, location: &strop_workspace::ResourceLocation) -> usize {
24 let Ok(target) = crate::files::FileTarget::from_location(location) else {
25 return self.config.tab_size;
26 };
27 self.docs
28 .iter()
29 .find(|(_, document)| document.matches_target(&target))
30 .map_or(self.config.tab_size, |(_, document)| document.indent.width)
31 }
32
33 fn indent_target_at(
34 &self,
35 document: strop_core::id::DocumentId,
36 byte: usize,
37 ) -> Option<strop_core::id::DocumentId> {
38 if let Some((source, _)) = self.source_position(document, byte) {
39 return Some(source);
40 }
41 let view = self.docs.get(document)?;
42 let collection = self.collections.get(&document)?;
43 match collection.rows.get(view.buf.line_of(byte)) {
44 Some(super::collections::CollectionRow::CardTop(index)) => collection
45 .excerpts
46 .get(*index)
47 .map(|excerpt| excerpt.source),
48 _ => None,
49 }
50 }
51
52 fn indent_command_target(&mut self) -> Option<strop_core::id::DocumentId> {
53 let target = self.indent_target_at(self.current(), self.head());
54 if target.is_none() {
55 self.message = "place the caret in a source excerpt to change indentation".into();
56 }
57 target
58 }
59
60 pub(crate) fn tab_size_command(&mut self, arg: &str) {
64 let arg = arg.trim();
65 if arg.is_empty() {
66 self.open_tab_size_picker();
67 return;
68 }
69 let Some(document) = self.indent_command_target() else {
70 return;
71 };
72 if arg.eq_ignore_ascii_case("auto") {
73 self.set_width_override(document, None);
74 return;
75 }
76 match arg.parse::<usize>() {
77 Ok(width)
78 if (crate::config::TAB_SIZE_MIN..=crate::config::TAB_SIZE_MAX).contains(&width) =>
79 {
80 self.set_width_override(document, Some(width));
81 }
82 _ => {
83 self.message = format!(
84 "tab-size takes a width {}–{} or auto, not {arg:?}",
85 crate::config::TAB_SIZE_MIN,
86 crate::config::TAB_SIZE_MAX,
87 );
88 }
89 }
90 }
91
92 pub(crate) fn indent_style_command(&mut self, arg: &str) {
95 if arg.trim().is_empty() {
96 self.open_tab_size_picker();
97 return;
98 }
99 let Some(document) = self.indent_command_target() else {
100 return;
101 };
102 match arg.trim() {
103 "spaces" => self.set_style_override(document, Some(IndentStyle::Spaces)),
104 "tabs" => self.set_style_override(document, Some(IndentStyle::Tabs)),
105 "auto" => self.set_style_override(document, None),
106 other => {
107 self.message = format!("indent-style takes spaces, tabs or auto, not {other:?}")
108 }
109 }
110 }
111
112 pub fn indent_label(&self) -> String {
114 self.indentation_at(self.current(), self.head()).label()
115 }
116
117 pub(crate) fn open_tab_size_picker(&mut self) {
122 let Some(document) = self.indent_command_target() else {
123 return;
124 };
125 let indent = self.doc(document).indent;
126 let current = |on: bool, source: IndentSource| {
127 if on {
128 format!(" — current ({})", source.label())
129 } else {
130 String::new()
131 }
132 };
133 let mut items: Vec<Item> = [2usize, 3, 4, 8]
134 .into_iter()
135 .map(|width| Item {
136 badge: Some(width.to_string()),
137 text: format!(
138 "spaces per indent{}",
139 current(
140 indent.style == IndentStyle::Spaces && indent.width == width,
141 indent.width_source,
142 )
143 ),
144 payload: Payload::IndentChoice(IndentChoice::Width(width)),
145 })
146 .collect();
147 items.push(Item {
148 badge: None,
149 text: format!(
150 "auto width — detect/configure{}",
151 current(
152 indent.width_source != IndentSource::Manual,
153 indent.width_source
154 )
155 ),
156 payload: Payload::IndentChoice(IndentChoice::AutoWidth),
157 });
158 items.push(Item {
159 badge: None,
160 text: format!(
161 "style: spaces{}",
162 current(indent.style == IndentStyle::Spaces, indent.style_source)
163 ),
164 payload: Payload::IndentChoice(IndentChoice::Spaces),
165 });
166 items.push(Item {
167 badge: None,
168 text: format!(
169 "style: tabs{}",
170 current(indent.style == IndentStyle::Tabs, indent.style_source)
171 ),
172 payload: Payload::IndentChoice(IndentChoice::Tabs),
173 });
174 items.push(Item {
175 badge: None,
176 text: format!(
177 "style: auto — detect/configure{}",
178 current(
179 indent.style_source != IndentSource::Manual,
180 indent.style_source
181 )
182 ),
183 payload: Payload::IndentChoice(IndentChoice::AutoStyle),
184 });
185 items.push(Item {
188 badge: None,
189 text: format!(
190 "custom width… — type {}–{}, enter",
191 crate::config::TAB_SIZE_MIN,
192 crate::config::TAB_SIZE_MAX
193 ),
194 payload: Payload::IndentChoice(IndentChoice::CustomWidth),
195 });
196 let mut picker = Picker::new(Kind::TabSize, items, false);
197 picker.pinned_tail = 1;
198 let mut glue = super::picker::PickerGlue::diagnostics(picker);
199 glue.indent_target = Some(document);
200 self.set_picker(glue);
201 }
202
203 pub(crate) fn accept_indent_choice(
206 &mut self,
207 document: strop_core::id::DocumentId,
208 choice: IndentChoice,
209 draft: &str,
210 ) {
211 match choice {
212 IndentChoice::Width(width) => self.set_width_override(document, Some(width)),
213 IndentChoice::AutoWidth => self.set_width_override(document, None),
214 IndentChoice::Spaces => self.set_style_override(document, Some(IndentStyle::Spaces)),
215 IndentChoice::Tabs => self.set_style_override(document, Some(IndentStyle::Tabs)),
216 IndentChoice::AutoStyle => self.set_style_override(document, None),
217 IndentChoice::CustomWidth => match draft.parse::<usize>() {
218 Ok(width)
219 if (crate::config::TAB_SIZE_MIN..=crate::config::TAB_SIZE_MAX)
220 .contains(&width) =>
221 {
222 self.set_width_override(document, Some(width));
223 }
224 _ if draft.is_empty() => {
225 self.message = format!(
226 "type a width {}–{} first, then enter",
227 crate::config::TAB_SIZE_MIN,
228 crate::config::TAB_SIZE_MAX,
229 );
230 }
231 _ => {
232 self.message = format!(
233 "tab-size takes a width {}–{}, not {draft:?}",
234 crate::config::TAB_SIZE_MIN,
235 crate::config::TAB_SIZE_MAX,
236 );
237 }
238 },
239 }
240 }
241
242 fn set_width_override(&mut self, document: strop_core::id::DocumentId, width: Option<usize>) {
243 let Some(doc) = self.docs.get_mut(document) else {
244 self.message = "indentation source was closed".into();
245 return;
246 };
247 doc.indent_override.width = width;
248 self.resolve_indent_for(document);
249 self.message = self.indent_status(document);
250 }
251
252 fn set_style_override(
253 &mut self,
254 document: strop_core::id::DocumentId,
255 style: Option<IndentStyle>,
256 ) {
257 let Some(doc) = self.docs.get_mut(document) else {
258 self.message = "indentation source was closed".into();
259 return;
260 };
261 doc.indent_override.style = style;
262 self.resolve_indent_for(document);
263 self.message = self.indent_status(document);
264 }
265
266 fn indent_status(&self, document: strop_core::id::DocumentId) -> String {
269 let indent = self.doc(document).indent;
270 format!(
271 "{}: {} (width {}, style {})",
272 self.doc(document).label(&self.cwd),
273 indent.label(),
274 indent.width_source.label(),
275 indent.style_source.label(),
276 )
277 }
278}