Skip to content

Commit 042f2e0

Browse files
authored
Fix index out-of-range exception when deleting script files (#748)
* Fix off-by-one in scriptfile implementation * Add obsolete warnings to deprecated APIs * Restore old file change validation logic
1 parent 13f2107 commit 042f2e0

File tree

2 files changed

+72
-33
lines changed

2 files changed

+72
-33
lines changed

src/PowerShellEditorServices/Workspace/ScriptFile.cs

+37-32
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public ScriptFileMarker[] SyntaxMarkers
9090
/// <summary>
9191
/// Gets the list of strings for each line of the file.
9292
/// </summary>
93-
internal IList<string> FileLines
93+
internal List<string> FileLines
9494
{
9595
get;
9696
private set;
@@ -197,7 +197,18 @@ public ScriptFile(
197197
/// </summary>
198198
/// <param name="text">Input string to be split up into lines.</param>
199199
/// <returns>The lines in the string.</returns>
200+
[Obsolete("This method is not designed for public exposure and will be retired in later versions of EditorServices")]
200201
public static IList<string> GetLines(string text)
202+
{
203+
return GetLinesInternal(text);
204+
}
205+
206+
/// <summary>
207+
/// Get the lines in a string.
208+
/// </summary>
209+
/// <param name="text">Input string to be split up into lines.</param>
210+
/// <returns>The lines in the string.</returns>
211+
internal static List<string> GetLinesInternal(string text)
201212
{
202213
if (text == null)
203214
{
@@ -311,38 +322,12 @@ public void ValidatePosition(BufferPosition bufferPosition)
311322
/// <param name="column">The 1-based column to be validated.</param>
312323
public void ValidatePosition(int line, int column)
313324
{
314-
ValidatePosition(line, column, isInsertion: false);
315-
}
316-
317-
/// <summary>
318-
/// Throws ArgumentOutOfRangeException if the given position is outside
319-
/// of the file's buffer extents. If the position is for an insertion (an applied change)
320-
/// the index may be 1 past the end of the file, which is just appended.
321-
/// </summary>
322-
/// <param name="line">The 1-based line to be validated.</param>
323-
/// <param name="column">The 1-based column to be validated.</param>
324-
/// <param name="isInsertion">If true, the position to validate is for an applied change.</param>
325-
public void ValidatePosition(int line, int column, bool isInsertion)
326-
{
327-
// If new content is being added, VSCode sometimes likes to add it at (FileLines.Count + 1),
328-
// which used to crash EditorServices. Now we append it on to the end of the file.
329-
// See https://github.com/PowerShell/vscode-powershell/issues/1283
330-
int maxLine = isInsertion ? this.FileLines.Count + 1 : this.FileLines.Count;
325+
int maxLine = this.FileLines.Count;
331326
if (line < 1 || line > maxLine)
332327
{
333328
throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the line range of 1 to {maxLine}.");
334329
}
335330

336-
// If we are inserting at the end of the file, the column should be 1
337-
if (isInsertion && line == maxLine)
338-
{
339-
if (column != 1)
340-
{
341-
throw new ArgumentOutOfRangeException($"Insertion at the end of a file must occur at column 1");
342-
}
343-
return;
344-
}
345-
346331
// The maximum column is either **one past** the length of the string
347332
// or 1 if the string is empty.
348333
string lineString = this.FileLines[line - 1];
@@ -354,6 +339,19 @@ public void ValidatePosition(int line, int column, bool isInsertion)
354339
}
355340
}
356341

342+
343+
/// <summary>
344+
/// Defunct ValidatePosition method call. The isInsertion parameter is ignored.
345+
/// </summary>
346+
/// <param name="line"></param>
347+
/// <param name="column"></param>
348+
/// <param name="isInsertion"></param>
349+
[Obsolete("Use ValidatePosition(int, int) instead")]
350+
public void ValidatePosition(int line, int column, bool isInsertion)
351+
{
352+
ValidatePosition(line, column);
353+
}
354+
357355
/// <summary>
358356
/// Applies the provided FileChange to the file's contents
359357
/// </summary>
@@ -373,9 +371,6 @@ public void ApplyChange(FileChange fileChange)
373371
}
374372
else
375373
{
376-
this.ValidatePosition(fileChange.Line, fileChange.Offset, isInsertion: true);
377-
this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset, isInsertion: true);
378-
379374
// VSCode sometimes likes to give the change start line as (FileLines.Count + 1).
380375
// This used to crash EditorServices, but we now treat it as an append.
381376
// See https://github.com/PowerShell/vscode-powershell/issues/1283
@@ -387,9 +382,19 @@ public void ApplyChange(FileChange fileChange)
387382
this.FileLines.Add(finalLine);
388383
}
389384
}
385+
// Similarly, when lines are deleted from the end of the file,
386+
// VSCode likes to give the end line as (FileLines.Count + 1).
387+
else if (fileChange.EndLine == this.FileLines.Count + 1 && String.Empty.Equals(fileChange.InsertString))
388+
{
389+
int lineIndex = fileChange.Line - 1;
390+
this.FileLines.RemoveRange(lineIndex, this.FileLines.Count - lineIndex);
391+
}
390392
// Otherwise, the change needs to go between existing content
391393
else
392394
{
395+
this.ValidatePosition(fileChange.Line, fileChange.Offset);
396+
this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset);
397+
393398
// Get the first fragment of the first line
394399
string firstLineFragment =
395400
this.FileLines[fileChange.Line - 1]
@@ -576,7 +581,7 @@ private void SetFileContents(string fileContents)
576581
{
577582
// Split the file contents into lines and trim
578583
// any carriage returns from the strings.
579-
this.FileLines = GetLines(fileContents);
584+
this.FileLines = GetLinesInternal(fileContents);
580585

581586
// Parse the contents to get syntax tree and errors
582587
this.ParseFileContents();

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+35-1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,23 @@ public void CanApplyEditsToEndOfFile()
143143
});
144144
}
145145

146+
[Fact]
147+
public void CanAppendToEndOfFile()
148+
{
149+
this.AssertFileChange(
150+
"line1\r\nline2\r\nline3",
151+
"line1\r\nline2\r\nline3\r\nline4\r\nline5",
152+
new FileChange
153+
{
154+
Line = 4,
155+
EndLine = 5,
156+
Offset = 1,
157+
EndOffset = 1,
158+
InsertString = "line4\r\nline5"
159+
}
160+
);
161+
}
162+
146163
[Fact]
147164
public void FindsDotSourcedFiles()
148165
{
@@ -181,14 +198,31 @@ public void ThrowsExceptionWithEditOutsideOfRange()
181198
new FileChange
182199
{
183200
Line = 3,
184-
EndLine = 7,
201+
EndLine = 8,
185202
Offset = 1,
186203
EndOffset = 1,
187204
InsertString = ""
188205
});
189206
});
190207
}
191208

209+
[Fact]
210+
public void CanDeleteFromEndOfFile()
211+
{
212+
this.AssertFileChange(
213+
"line1\r\nline2\r\nline3\r\nline4",
214+
"line1\r\nline2",
215+
new FileChange
216+
{
217+
Line = 3,
218+
EndLine = 5,
219+
Offset = 1,
220+
EndOffset = 1,
221+
InsertString = ""
222+
}
223+
);
224+
}
225+
192226
internal static ScriptFile CreateScriptFile(string initialString)
193227
{
194228
using (StringReader stringReader = new StringReader(initialString))

0 commit comments

Comments
 (0)