Skip to content

Commit d60e96f

Browse files
authored
Fix crash where lines appended to end of script file causes out of bounds exception (#730)
1 parent 90999e4 commit d60e96f

File tree

2 files changed

+99
-43
lines changed

2 files changed

+99
-43
lines changed

src/PowerShellEditorServices/Workspace/ScriptFile.cs

+80-40
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public Token[] ScriptTokens
114114
}
115115

116116
/// <summary>
117-
/// Gets the array of filepaths dot sourced in this ScriptFile
117+
/// Gets the array of filepaths dot sourced in this ScriptFile
118118
/// </summary>
119119
public string[] ReferencedFiles
120120
{
@@ -227,7 +227,7 @@ public static IList<string> GetLines(string text)
227227
}
228228

229229
/// <summary>
230-
/// Deterines whether the supplied path indicates the file is an "untitled:Unitled-X"
230+
/// Deterines whether the supplied path indicates the file is an "untitled:Unitled-X"
231231
/// which has not been saved to file.
232232
/// </summary>
233233
/// <param name="path">The path to check.</param>
@@ -311,12 +311,38 @@ public void ValidatePosition(BufferPosition bufferPosition)
311311
/// <param name="column">The 1-based column to be validated.</param>
312312
public void ValidatePosition(int line, int column)
313313
{
314-
int maxLine = this.FileLines.Count;
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;
315331
if (line < 1 || line > maxLine)
316332
{
317333
throw new ArgumentOutOfRangeException($"Position {line}:{column} is outside of the line range of 1 to {maxLine}.");
318334
}
319335

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+
320346
// The maximum column is either **one past** the length of the string
321347
// or 1 if the string is empty.
322348
string lineString = this.FileLines[line - 1];
@@ -347,51 +373,65 @@ public void ApplyChange(FileChange fileChange)
347373
}
348374
else
349375
{
350-
this.ValidatePosition(fileChange.Line, fileChange.Offset);
351-
this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset);
352-
353-
// Get the first fragment of the first line
354-
string firstLineFragment =
355-
this.FileLines[fileChange.Line - 1]
356-
.Substring(0, fileChange.Offset - 1);
357-
358-
// Get the last fragment of the last line
359-
string endLine = this.FileLines[fileChange.EndLine - 1];
360-
string lastLineFragment =
361-
endLine.Substring(
362-
fileChange.EndOffset - 1,
363-
(this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1);
364-
365-
// Remove the old lines
366-
for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++)
367-
{
368-
this.FileLines.RemoveAt(fileChange.Line - 1);
369-
}
376+
this.ValidatePosition(fileChange.Line, fileChange.Offset, isInsertion: true);
377+
this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset, isInsertion: true);
370378

371-
// Build and insert the new lines
372-
int currentLineNumber = fileChange.Line;
373-
for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++)
379+
// VSCode sometimes likes to give the change start line as (FileLines.Count + 1).
380+
// This used to crash EditorServices, but we now treat it as an append.
381+
// See https://github.com/PowerShell/vscode-powershell/issues/1283
382+
if (fileChange.Line == this.FileLines.Count + 1)
374383
{
375-
// Since we split the lines above using \n, make sure to
376-
// trim the ending \r's off as well.
377-
string finalLine = changeLines[changeIndex].TrimEnd('\r');
378-
379-
// Should we add first or last line fragments?
380-
if (changeIndex == 0)
384+
foreach (string addedLine in changeLines)
381385
{
382-
// Append the first line fragment
383-
finalLine = firstLineFragment + finalLine;
386+
string finalLine = addedLine.TrimEnd('\r');
387+
this.FileLines.Add(finalLine);
384388
}
385-
if (changeIndex == changeLines.Length - 1)
389+
}
390+
// Otherwise, the change needs to go between existing content
391+
else
392+
{
393+
// Get the first fragment of the first line
394+
string firstLineFragment =
395+
this.FileLines[fileChange.Line - 1]
396+
.Substring(0, fileChange.Offset - 1);
397+
398+
// Get the last fragment of the last line
399+
string endLine = this.FileLines[fileChange.EndLine - 1];
400+
string lastLineFragment =
401+
endLine.Substring(
402+
fileChange.EndOffset - 1,
403+
(this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1);
404+
405+
// Remove the old lines
406+
for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++)
386407
{
387-
// Append the last line fragment
388-
finalLine = finalLine + lastLineFragment;
408+
this.FileLines.RemoveAt(fileChange.Line - 1);
389409
}
390410

391-
this.FileLines.Insert(currentLineNumber - 1, finalLine);
392-
currentLineNumber++;
393-
}
411+
// Build and insert the new lines
412+
int currentLineNumber = fileChange.Line;
413+
for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++)
414+
{
415+
// Since we split the lines above using \n, make sure to
416+
// trim the ending \r's off as well.
417+
string finalLine = changeLines[changeIndex].TrimEnd('\r');
394418

419+
// Should we add first or last line fragments?
420+
if (changeIndex == 0)
421+
{
422+
// Append the first line fragment
423+
finalLine = firstLineFragment + finalLine;
424+
}
425+
if (changeIndex == changeLines.Length - 1)
426+
{
427+
// Append the last line fragment
428+
finalLine = finalLine + lastLineFragment;
429+
}
430+
431+
this.FileLines.Insert(currentLineNumber - 1, finalLine);
432+
currentLineNumber++;
433+
}
434+
}
395435
}
396436

397437
// Parse the script again to be up-to-date

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+19-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace PSLanguageService.Test
1313
{
1414
public class ScriptFileChangeTests
1515
{
16-
private static readonly Version PowerShellVersion = new Version("5.0");
16+
private static readonly Version PowerShellVersion = new Version("5.0");
1717

1818
[Fact]
1919
public void CanApplySingleLineInsert()
@@ -127,6 +127,22 @@ public void CanApplyMultiLineDelete()
127127
});
128128
}
129129

130+
[Fact]
131+
public void CanApplyEditsToEndOfFile()
132+
{
133+
this.AssertFileChange(
134+
"line1\r\nline2\r\nline3\r\n\r\n",
135+
"line1\r\nline2\r\nline3\r\n\r\n\r\n\r\n",
136+
new FileChange
137+
{
138+
Line = 5,
139+
EndLine = 5,
140+
Offset = 1,
141+
EndOffset = 1,
142+
InsertString = "\r\n\r\n"
143+
});
144+
}
145+
130146
[Fact]
131147
public void FindsDotSourcedFiles()
132148
{
@@ -139,7 +155,7 @@ public void FindsDotSourcedFiles()
139155

140156
using (StringReader stringReader = new StringReader(exampleScriptContents))
141157
{
142-
ScriptFile scriptFile =
158+
ScriptFile scriptFile =
143159
new ScriptFile(
144160
"DotSourceTestFile.ps1",
145161
"DotSourceTestFile.ps1",
@@ -178,7 +194,7 @@ internal static ScriptFile CreateScriptFile(string initialString)
178194
using (StringReader stringReader = new StringReader(initialString))
179195
{
180196
// Create an in-memory file from the StringReader
181-
ScriptFile fileToChange =
197+
ScriptFile fileToChange =
182198
new ScriptFile(
183199
"TestFile.ps1",
184200
"TestFile.ps1",

0 commit comments

Comments
 (0)