1 | // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team |
---|
2 | // |
---|
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this |
---|
4 | // software and associated documentation files (the "Software"), to deal in the Software |
---|
5 | // without restriction, including without limitation the rights to use, copy, modify, merge, |
---|
6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
---|
7 | // to whom the Software is furnished to do so, subject to the following conditions: |
---|
8 | // |
---|
9 | // The above copyright notice and this permission notice shall be included in all copies or |
---|
10 | // substantial portions of the Software. |
---|
11 | // |
---|
12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
---|
13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
---|
14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
---|
15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
---|
16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
---|
17 | // DEALINGS IN THE SOFTWARE. |
---|
18 | |
---|
19 | using System; |
---|
20 | using System.Collections.Generic; |
---|
21 | using System.IO; |
---|
22 | using System.Linq; |
---|
23 | using System.Text; |
---|
24 | using System.Windows; |
---|
25 | using System.Windows.Documents; |
---|
26 | using System.Windows.Input; |
---|
27 | using System.Windows.Media.TextFormatting; |
---|
28 | using ICSharpCode.AvalonEdit.Document; |
---|
29 | using ICSharpCode.AvalonEdit.Rendering; |
---|
30 | using ICSharpCode.AvalonEdit.Utils; |
---|
31 | #if NREFACTORY |
---|
32 | using ICSharpCode.NRefactory; |
---|
33 | using ICSharpCode.NRefactory.Editor; |
---|
34 | #endif |
---|
35 | |
---|
36 | namespace ICSharpCode.AvalonEdit.Editing |
---|
37 | { |
---|
38 | /// <summary> |
---|
39 | /// Rectangular selection ("box selection"). |
---|
40 | /// </summary> |
---|
41 | public sealed class RectangleSelection : Selection |
---|
42 | { |
---|
43 | #region Commands |
---|
44 | /// <summary> |
---|
45 | /// Expands the selection left by one character, creating a rectangular selection. |
---|
46 | /// Key gesture: Alt+Shift+Left |
---|
47 | /// </summary> |
---|
48 | public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter"); |
---|
49 | |
---|
50 | /// <summary> |
---|
51 | /// Expands the selection right by one character, creating a rectangular selection. |
---|
52 | /// Key gesture: Alt+Shift+Right |
---|
53 | /// </summary> |
---|
54 | public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter"); |
---|
55 | |
---|
56 | /// <summary> |
---|
57 | /// Expands the selection left by one word, creating a rectangular selection. |
---|
58 | /// Key gesture: Ctrl+Alt+Shift+Left |
---|
59 | /// </summary> |
---|
60 | public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord"); |
---|
61 | |
---|
62 | /// <summary> |
---|
63 | /// Expands the selection left by one word, creating a rectangular selection. |
---|
64 | /// Key gesture: Ctrl+Alt+Shift+Right |
---|
65 | /// </summary> |
---|
66 | public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord"); |
---|
67 | |
---|
68 | /// <summary> |
---|
69 | /// Expands the selection up by one line, creating a rectangular selection. |
---|
70 | /// Key gesture: Alt+Shift+Up |
---|
71 | /// </summary> |
---|
72 | public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine"); |
---|
73 | |
---|
74 | /// <summary> |
---|
75 | /// Expands the selection up by one line, creating a rectangular selection. |
---|
76 | /// Key gesture: Alt+Shift+Down |
---|
77 | /// </summary> |
---|
78 | public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine"); |
---|
79 | |
---|
80 | /// <summary> |
---|
81 | /// Expands the selection to the start of the line, creating a rectangular selection. |
---|
82 | /// Key gesture: Alt+Shift+Home |
---|
83 | /// </summary> |
---|
84 | public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart"); |
---|
85 | |
---|
86 | /// <summary> |
---|
87 | /// Expands the selection to the end of the line, creating a rectangular selection. |
---|
88 | /// Key gesture: Alt+Shift+End |
---|
89 | /// </summary> |
---|
90 | public static readonly RoutedUICommand BoxSelectToLineEnd = Command("BoxSelectToLineEnd"); |
---|
91 | |
---|
92 | static RoutedUICommand Command(string name) |
---|
93 | { |
---|
94 | return new RoutedUICommand(name, name, typeof(RectangleSelection)); |
---|
95 | } |
---|
96 | #endregion |
---|
97 | |
---|
98 | TextDocument document; |
---|
99 | readonly int startLine, endLine; |
---|
100 | readonly double startXPos, endXPos; |
---|
101 | readonly int topLeftOffset, bottomRightOffset; |
---|
102 | readonly TextViewPosition start, end; |
---|
103 | |
---|
104 | readonly List<SelectionSegment> segments = new List<SelectionSegment>(); |
---|
105 | |
---|
106 | #region Constructors |
---|
107 | /// <summary> |
---|
108 | /// Creates a new rectangular selection. |
---|
109 | /// </summary> |
---|
110 | public RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end) |
---|
111 | : base(textArea) |
---|
112 | { |
---|
113 | InitDocument(); |
---|
114 | this.startLine = start.Line; |
---|
115 | this.endLine = end.Line; |
---|
116 | this.startXPos = GetXPos(textArea, start); |
---|
117 | this.endXPos = GetXPos(textArea, end); |
---|
118 | CalculateSegments(); |
---|
119 | this.topLeftOffset = this.segments.First().StartOffset; |
---|
120 | this.bottomRightOffset = this.segments.Last().EndOffset; |
---|
121 | |
---|
122 | this.start = start; |
---|
123 | this.end = end; |
---|
124 | } |
---|
125 | |
---|
126 | private RectangleSelection(TextArea textArea, int startLine, double startXPos, TextViewPosition end) |
---|
127 | : base(textArea) |
---|
128 | { |
---|
129 | InitDocument(); |
---|
130 | this.startLine = startLine; |
---|
131 | this.endLine = end.Line; |
---|
132 | this.startXPos = startXPos; |
---|
133 | this.endXPos = GetXPos(textArea, end); |
---|
134 | CalculateSegments(); |
---|
135 | this.topLeftOffset = this.segments.First().StartOffset; |
---|
136 | this.bottomRightOffset = this.segments.Last().EndOffset; |
---|
137 | |
---|
138 | this.start = GetStart(); |
---|
139 | this.end = end; |
---|
140 | } |
---|
141 | |
---|
142 | private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, double endXPos) |
---|
143 | : base(textArea) |
---|
144 | { |
---|
145 | InitDocument(); |
---|
146 | this.startLine = start.Line; |
---|
147 | this.endLine = endLine; |
---|
148 | this.startXPos = GetXPos(textArea, start); |
---|
149 | this.endXPos = endXPos; |
---|
150 | CalculateSegments(); |
---|
151 | this.topLeftOffset = this.segments.First().StartOffset; |
---|
152 | this.bottomRightOffset = this.segments.Last().EndOffset; |
---|
153 | |
---|
154 | this.start = start; |
---|
155 | this.end = GetEnd(); |
---|
156 | } |
---|
157 | |
---|
158 | void InitDocument() |
---|
159 | { |
---|
160 | document = textArea.Document; |
---|
161 | if (document == null) |
---|
162 | throw ThrowUtil.NoDocumentAssigned(); |
---|
163 | } |
---|
164 | |
---|
165 | static double GetXPos(TextArea textArea, TextViewPosition pos) |
---|
166 | { |
---|
167 | DocumentLine documentLine = textArea.Document.GetLineByNumber(pos.Line); |
---|
168 | VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine); |
---|
169 | int vc = visualLine.ValidateVisualColumn(pos, true); |
---|
170 | TextLine textLine = visualLine.GetTextLine(vc, pos.IsAtEndOfLine); |
---|
171 | return visualLine.GetTextLineVisualXPosition(textLine, vc); |
---|
172 | } |
---|
173 | |
---|
174 | void CalculateSegments() |
---|
175 | { |
---|
176 | DocumentLine nextLine = document.GetLineByNumber(Math.Min(startLine, endLine)); |
---|
177 | do { |
---|
178 | VisualLine vl = textArea.TextView.GetOrConstructVisualLine(nextLine); |
---|
179 | int startVC = vl.GetVisualColumn(new Point(startXPos, 0), true); |
---|
180 | int endVC = vl.GetVisualColumn(new Point(endXPos, 0), true); |
---|
181 | |
---|
182 | int baseOffset = vl.FirstDocumentLine.Offset; |
---|
183 | int startOffset = baseOffset + vl.GetRelativeOffset(startVC); |
---|
184 | int endOffset = baseOffset + vl.GetRelativeOffset(endVC); |
---|
185 | segments.Add(new SelectionSegment(startOffset, startVC, endOffset, endVC)); |
---|
186 | |
---|
187 | nextLine = vl.LastDocumentLine.NextLine; |
---|
188 | } while (nextLine != null && nextLine.LineNumber <= Math.Max(startLine, endLine)); |
---|
189 | } |
---|
190 | |
---|
191 | TextViewPosition GetStart() |
---|
192 | { |
---|
193 | SelectionSegment segment = (startLine < endLine ? segments.First() : segments.Last()); |
---|
194 | if (startXPos < endXPos) { |
---|
195 | return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); |
---|
196 | } else { |
---|
197 | return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); |
---|
198 | } |
---|
199 | } |
---|
200 | |
---|
201 | TextViewPosition GetEnd() |
---|
202 | { |
---|
203 | SelectionSegment segment = (startLine < endLine ? segments.Last() : segments.First()); |
---|
204 | if (startXPos < endXPos) { |
---|
205 | return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); |
---|
206 | } else { |
---|
207 | return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); |
---|
208 | } |
---|
209 | } |
---|
210 | #endregion |
---|
211 | |
---|
212 | /// <inheritdoc/> |
---|
213 | public override string GetText() |
---|
214 | { |
---|
215 | StringBuilder b = new StringBuilder(); |
---|
216 | foreach (ISegment s in this.Segments) { |
---|
217 | if (b.Length > 0) |
---|
218 | b.AppendLine(); |
---|
219 | b.Append(document.GetText(s)); |
---|
220 | } |
---|
221 | return b.ToString(); |
---|
222 | } |
---|
223 | |
---|
224 | /// <inheritdoc/> |
---|
225 | public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) |
---|
226 | { |
---|
227 | return SetEndpoint(endPosition); |
---|
228 | } |
---|
229 | |
---|
230 | /// <inheritdoc/> |
---|
231 | public override int Length { |
---|
232 | get { |
---|
233 | return this.Segments.Sum(s => s.Length); |
---|
234 | } |
---|
235 | } |
---|
236 | |
---|
237 | /// <inheritdoc/> |
---|
238 | public override bool EnableVirtualSpace { |
---|
239 | get { return true; } |
---|
240 | } |
---|
241 | |
---|
242 | /// <inheritdoc/> |
---|
243 | public override ISegment SurroundingSegment { |
---|
244 | get { |
---|
245 | return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset); |
---|
246 | } |
---|
247 | } |
---|
248 | |
---|
249 | /// <inheritdoc/> |
---|
250 | public override IEnumerable<SelectionSegment> Segments { |
---|
251 | get { return segments; } |
---|
252 | } |
---|
253 | |
---|
254 | /// <inheritdoc/> |
---|
255 | public override TextViewPosition StartPosition { |
---|
256 | get { return start; } |
---|
257 | } |
---|
258 | |
---|
259 | /// <inheritdoc/> |
---|
260 | public override TextViewPosition EndPosition { |
---|
261 | get { return end; } |
---|
262 | } |
---|
263 | |
---|
264 | /// <inheritdoc/> |
---|
265 | public override bool Equals(object obj) |
---|
266 | { |
---|
267 | RectangleSelection r = obj as RectangleSelection; |
---|
268 | return r != null && r.textArea == this.textArea |
---|
269 | && r.topLeftOffset == this.topLeftOffset && r.bottomRightOffset == this.bottomRightOffset |
---|
270 | && r.startLine == this.startLine && r.endLine == this.endLine |
---|
271 | && r.startXPos == this.startXPos && r.endXPos == this.endXPos; |
---|
272 | } |
---|
273 | |
---|
274 | /// <inheritdoc/> |
---|
275 | public override int GetHashCode() |
---|
276 | { |
---|
277 | return topLeftOffset ^ bottomRightOffset; |
---|
278 | } |
---|
279 | |
---|
280 | /// <inheritdoc/> |
---|
281 | public override Selection SetEndpoint(TextViewPosition endPosition) |
---|
282 | { |
---|
283 | return new RectangleSelection(textArea, startLine, startXPos, endPosition); |
---|
284 | } |
---|
285 | |
---|
286 | int GetVisualColumnFromXPos(int line, double xPos) |
---|
287 | { |
---|
288 | var vl = textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(line)); |
---|
289 | return vl.GetVisualColumn(new Point(xPos, 0), true); |
---|
290 | } |
---|
291 | |
---|
292 | /// <inheritdoc/> |
---|
293 | public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) |
---|
294 | { |
---|
295 | TextLocation newStartLocation = textArea.Document.GetLocation(e.GetNewOffset(topLeftOffset, AnchorMovementType.AfterInsertion)); |
---|
296 | TextLocation newEndLocation = textArea.Document.GetLocation(e.GetNewOffset(bottomRightOffset, AnchorMovementType.BeforeInsertion)); |
---|
297 | |
---|
298 | return new RectangleSelection(textArea, |
---|
299 | new TextViewPosition(newStartLocation, GetVisualColumnFromXPos(newStartLocation.Line, startXPos)), |
---|
300 | new TextViewPosition(newEndLocation, GetVisualColumnFromXPos(newEndLocation.Line, endXPos))); |
---|
301 | } |
---|
302 | |
---|
303 | /// <inheritdoc/> |
---|
304 | public override void ReplaceSelectionWithText(string newText) |
---|
305 | { |
---|
306 | if (newText == null) |
---|
307 | throw new ArgumentNullException("newText"); |
---|
308 | using (textArea.Document.RunUpdate()) { |
---|
309 | TextViewPosition start = new TextViewPosition(document.GetLocation(topLeftOffset), GetVisualColumnFromXPos(startLine, startXPos)); |
---|
310 | TextViewPosition end = new TextViewPosition(document.GetLocation(bottomRightOffset), GetVisualColumnFromXPos(endLine, endXPos)); |
---|
311 | int insertionLength; |
---|
312 | int totalInsertionLength = 0; |
---|
313 | int firstInsertionLength = 0; |
---|
314 | int editOffset = Math.Min(topLeftOffset, bottomRightOffset); |
---|
315 | TextViewPosition pos; |
---|
316 | if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) { |
---|
317 | // insert same text into every line |
---|
318 | foreach (SelectionSegment lineSegment in this.Segments.Reverse()) { |
---|
319 | ReplaceSingleLineText(textArea, lineSegment, newText, out insertionLength); |
---|
320 | totalInsertionLength += insertionLength; |
---|
321 | firstInsertionLength = insertionLength; |
---|
322 | } |
---|
323 | |
---|
324 | int newEndOffset = editOffset + totalInsertionLength; |
---|
325 | pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); |
---|
326 | |
---|
327 | textArea.Selection = new RectangleSelection(textArea, pos, Math.Max(startLine, endLine), GetXPos(textArea, pos)); |
---|
328 | } else { |
---|
329 | string[] lines = newText.Split(NewLineFinder.NewlineStrings, segments.Count, StringSplitOptions.None); |
---|
330 | int line = Math.Min(startLine, endLine); |
---|
331 | for (int i = lines.Length - 1; i >= 0; i--) { |
---|
332 | ReplaceSingleLineText(textArea, segments[i], lines[i], out insertionLength); |
---|
333 | firstInsertionLength = insertionLength; |
---|
334 | } |
---|
335 | pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); |
---|
336 | textArea.ClearSelection(); |
---|
337 | } |
---|
338 | textArea.Caret.Position = textArea.TextView.GetPosition(new Point(GetXPos(textArea, pos), textArea.TextView.GetVisualTopByDocumentLine(Math.Max(startLine, endLine)))).GetValueOrDefault(); |
---|
339 | } |
---|
340 | } |
---|
341 | |
---|
342 | void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegment, string newText, out int insertionLength) |
---|
343 | { |
---|
344 | if (lineSegment.Length == 0) { |
---|
345 | if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset)) { |
---|
346 | newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); |
---|
347 | textArea.Document.Insert(lineSegment.StartOffset, newText); |
---|
348 | } |
---|
349 | } else { |
---|
350 | ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment); |
---|
351 | for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { |
---|
352 | if (i == segmentsToDelete.Length - 1) { |
---|
353 | if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) { |
---|
354 | newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); |
---|
355 | } |
---|
356 | textArea.Document.Replace(segmentsToDelete[i], newText); |
---|
357 | } else { |
---|
358 | textArea.Document.Remove(segmentsToDelete[i]); |
---|
359 | } |
---|
360 | } |
---|
361 | } |
---|
362 | insertionLength = newText.Length; |
---|
363 | } |
---|
364 | |
---|
365 | /// <summary> |
---|
366 | /// Performs a rectangular paste operation. |
---|
367 | /// </summary> |
---|
368 | public static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText) |
---|
369 | { |
---|
370 | if (textArea == null) |
---|
371 | throw new ArgumentNullException("textArea"); |
---|
372 | if (text == null) |
---|
373 | throw new ArgumentNullException("text"); |
---|
374 | int newLineCount = text.Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today. |
---|
375 | TextLocation endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column); |
---|
376 | if (endLocation.Line <= textArea.Document.LineCount) { |
---|
377 | int endOffset = textArea.Document.GetOffset(endLocation); |
---|
378 | if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation) { |
---|
379 | RectangleSelection rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition)); |
---|
380 | rsel.ReplaceSelectionWithText(text); |
---|
381 | if (selectInsertedText && textArea.Selection is RectangleSelection) { |
---|
382 | RectangleSelection sel = (RectangleSelection)textArea.Selection; |
---|
383 | textArea.Selection = new RectangleSelection(textArea, startPosition, sel.endLine, sel.endXPos); |
---|
384 | } |
---|
385 | return true; |
---|
386 | } |
---|
387 | } |
---|
388 | return false; |
---|
389 | } |
---|
390 | |
---|
391 | /// <summary> |
---|
392 | /// Gets the name of the entry in the DataObject that signals rectangle selections. |
---|
393 | /// </summary> |
---|
394 | public const string RectangularSelectionDataType = "AvalonEditRectangularSelection"; |
---|
395 | |
---|
396 | /// <inheritdoc/> |
---|
397 | public override System.Windows.DataObject CreateDataObject(TextArea textArea) |
---|
398 | { |
---|
399 | var data = base.CreateDataObject(textArea); |
---|
400 | |
---|
401 | if (EditingCommandHandler.ConfirmDataFormat(textArea, data, RectangularSelectionDataType)) { |
---|
402 | MemoryStream isRectangle = new MemoryStream(1); |
---|
403 | isRectangle.WriteByte(1); |
---|
404 | data.SetData(RectangularSelectionDataType, isRectangle, false); |
---|
405 | } |
---|
406 | return data; |
---|
407 | } |
---|
408 | |
---|
409 | /// <inheritdoc/> |
---|
410 | public override string ToString() |
---|
411 | { |
---|
412 | // It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message |
---|
413 | // make sure we don't crash even when the desired locations don't exist anymore. |
---|
414 | return string.Format("[RectangleSelection {0} {1} {2} to {3} {4} {5}]", startLine, topLeftOffset, startXPos, endLine, bottomRightOffset, endXPos); |
---|
415 | } |
---|
416 | } |
---|
417 | } |
---|