Skip to content

Commit e3d46b5

Browse files
committed
Fix #45, #63: Completion text replacement bugs
This change resolves a general class of issues around code completions coming from the language service. The JSON protocol allows the language server to specify the range of code that will get replaced by a completion item. Previously this range was being computed incorrectly. We now use the replacement range given by the PowerShell API. This also resolves PowerShell/vscode-powershell issues 45 and 12
1 parent edbebbf commit e3d46b5

File tree

13 files changed

+287
-46
lines changed

13 files changed

+287
-46
lines changed

Diff for: src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+6-33
Original file line numberDiff line numberDiff line change
@@ -364,38 +364,13 @@ await editorSession.LanguageService.GetCompletionsInFile(
364364
int startEditColumn = textDocumentPosition.Position.Character;
365365
int endEditColumn = textDocumentPosition.Position.Character;
366366

367-
// Find the extents of the token under the cursor
368-
var completedToken =
369-
scriptFile
370-
.ScriptAst
371-
.FindAll(
372-
ast =>
373-
{
374-
return
375-
!(ast is PipelineAst) &&
376-
ast.Extent.StartLineNumber == cursorLine &&
377-
ast.Extent.StartColumnNumber <= cursorColumn &&
378-
ast.Extent.EndColumnNumber >= cursorColumn;
379-
},
380-
true)
381-
.LastOrDefault(); // The most relevant AST will be the last
382-
383-
if (completedToken != null)
384-
{
385-
// The edit should replace the token that was found at the cursor position
386-
startEditColumn = completedToken.Extent.StartColumnNumber - 1;
387-
endEditColumn = completedToken.Extent.EndColumnNumber - 1;
388-
}
389-
390367
completionItems =
391368
completionResults
392369
.Completions
393370
.Select(
394371
c => CreateCompletionItem(
395372
c,
396-
textDocumentPosition.Position.Line,
397-
startEditColumn,
398-
endEditColumn))
373+
completionResults.ReplacedRange))
399374
.ToArray();
400375
}
401376
else
@@ -942,9 +917,7 @@ private static CompletionItemKind MapCompletionKind(CompletionType completionTyp
942917

943918
private static CompletionItem CreateCompletionItem(
944919
CompletionDetails completionDetails,
945-
int lineNumber,
946-
int startColumn,
947-
int endColumn)
920+
BufferRange completionRange)
948921
{
949922
string detailString = null;
950923

@@ -971,13 +944,13 @@ private static CompletionItem CreateCompletionItem(
971944
{
972945
Start = new Position
973946
{
974-
Line = lineNumber,
975-
Character = startColumn
947+
Line = completionRange.Start.Line - 1,
948+
Character = completionRange.Start.Column - 1
976949
},
977950
End = new Position
978951
{
979-
Line = lineNumber,
980-
Character = endColumn
952+
Line = completionRange.End.Line - 1,
953+
Character = completionRange.End.Column - 1
981954
}
982955
}
983956
}

Diff for: src/PowerShellEditorServices/Language/AstOperations.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ internal static class AstOperations
3838
/// A CommandCompletion instance that contains completions for the
3939
/// symbol at the given offset.
4040
/// </returns>
41-
static public CompletionResults GetCompletions(
41+
static public CommandCompletion GetCompletions(
4242
Ast scriptAst,
4343
Token[] currentTokens,
44-
int fileOffset,
44+
int fileOffset,
4545
Runspace runspace)
4646
{
4747
var type = scriptAst.Extent.StartScriptPosition.GetType();
@@ -75,7 +75,7 @@ static public CompletionResults GetCompletions(
7575
}
7676
}
7777

78-
return CompletionResults.Create(commandCompletion);
78+
return commandCompletion;
7979
}
8080

8181
/// <summary>

Diff for: src/PowerShellEditorServices/Language/CompletionResults.cs

+20
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,36 @@ public sealed class CompletionResults
2424
/// </summary>
2525
public CompletionDetails[] Completions { get; private set; }
2626

27+
/// <summary>
28+
/// Gets the range in the buffer that should be replaced by this
29+
/// completion result.
30+
/// </summary>
31+
public BufferRange ReplacedRange { get; private set; }
32+
2733
#endregion
2834

2935
#region Constructors
3036

37+
/// <summary>
38+
/// Creates an empty CompletionResults instance.
39+
/// </summary>
40+
public CompletionResults()
41+
{
42+
this.Completions = new CompletionDetails[0];
43+
this.ReplacedRange = new BufferRange();
44+
}
45+
3146
internal static CompletionResults Create(
47+
ScriptFile scriptFile,
3248
CommandCompletion commandCompletion)
3349
{
3450
return new CompletionResults
3551
{
3652
Completions = GetCompletionsArray(commandCompletion),
53+
ReplacedRange =
54+
scriptFile.GetRangeBetweenOffsets(
55+
commandCompletion.ReplacementIndex,
56+
commandCompletion.ReplacementIndex + commandCompletion.ReplacementLength)
3757
};
3858
}
3959

Diff for: src/PowerShellEditorServices/Language/LanguageService.cs

+21-9
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,34 @@ public async Task<CompletionResults> GetCompletionsInFile(
8989
RunspaceHandle runspaceHandle =
9090
await this.powerShellContext.GetRunspaceHandle();
9191

92-
CompletionResults completionResults =
92+
CommandCompletion commandCompletion =
9393
AstOperations.GetCompletions(
9494
scriptFile.ScriptAst,
9595
scriptFile.ScriptTokens,
9696
fileOffset,
9797
runspaceHandle.Runspace);
9898

9999
runspaceHandle.Dispose();
100-
101-
// save state of most recent completion
102-
mostRecentCompletions = completionResults;
103-
mostRecentRequestFile = scriptFile.Id;
104-
mostRecentRequestLine = lineNumber;
105-
mostRecentRequestOffest = columnNumber;
106-
107-
return completionResults;
100+
101+
if (commandCompletion != null)
102+
{
103+
CompletionResults completionResults =
104+
CompletionResults.Create(
105+
scriptFile,
106+
commandCompletion);
107+
108+
// save state of most recent completion
109+
mostRecentCompletions = completionResults;
110+
mostRecentRequestFile = scriptFile.Id;
111+
mostRecentRequestLine = lineNumber;
112+
mostRecentRequestOffest = columnNumber;
113+
114+
return completionResults;
115+
}
116+
else
117+
{
118+
return new CompletionResults();
119+
}
108120
}
109121

110122
/// <summary>

Diff for: src/PowerShellEditorServices/PowerShellEditorServices.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@
102102
<Compile Include="Session\SynchronizingConsoleHostWrapper.cs" />
103103
<Compile Include="Utility\Logger.cs" />
104104
<Compile Include="Utility\Validate.cs" />
105+
<Compile Include="Workspace\BufferRange.cs" />
105106
<Compile Include="Workspace\FileChange.cs" />
107+
<Compile Include="Workspace\BufferPosition.cs" />
106108
<Compile Include="Workspace\ScriptFile.cs" />
107109
<Compile Include="Workspace\ScriptFileMarker.cs" />
108110
<Compile Include="Workspace\ScriptRegion.cs" />
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Microsoft.PowerShell.EditorServices
7+
{
8+
/// <summary>
9+
/// Provides details about a position in a file buffer.
10+
/// </summary>
11+
public struct BufferPosition
12+
{
13+
/// <summary>
14+
/// Gets the line number of the position in the buffer.
15+
/// </summary>
16+
public int Line { get; private set; }
17+
18+
/// <summary>
19+
/// Gets the column number of the position in the buffer.
20+
/// </summary>
21+
public int Column { get; private set; }
22+
23+
/// <summary>
24+
/// Creates a new instance of the BufferPosition class.
25+
/// </summary>
26+
/// <param name="line">The line number of the position.</param>
27+
/// <param name="column">The column number of the position.</param>
28+
public BufferPosition(int line, int column)
29+
{
30+
this.Line = line;
31+
this.Column = column;
32+
}
33+
}
34+
}
35+
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Microsoft.PowerShell.EditorServices
7+
{
8+
/// <summary>
9+
/// Provides details about a range between two positions in
10+
/// a file buffer.
11+
/// </summary>
12+
public struct BufferRange
13+
{
14+
/// <summary>
15+
/// Gets the start position of the range in the buffer.
16+
/// </summary>
17+
public BufferPosition Start { get; private set; }
18+
19+
/// <summary>
20+
/// Gets the end position of the range in the buffer.
21+
/// </summary>
22+
public BufferPosition End { get; private set; }
23+
24+
/// <summary>
25+
/// Creates a new instance of the BufferRange class.
26+
/// </summary>
27+
/// <param name="start">The start position of the range.</param>
28+
/// <param name="end">The end position of the range.</param>
29+
public BufferRange(BufferPosition start, BufferPosition end)
30+
{
31+
this.Start = start;
32+
this.End = end;
33+
}
34+
}
35+
}
36+

Diff for: src/PowerShellEditorServices/Workspace/ScriptFile.cs

+75
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,81 @@ public int GetOffsetAtPosition(int lineNumber, int columnNumber)
255255
return offset;
256256
}
257257

258+
/// <summary>
259+
/// Calculates the 1-based line and column number position based
260+
/// on the given buffer offset.
261+
/// </summary>
262+
/// <param name="bufferOffset">The buffer offset to convert.</param>
263+
/// <returns>A new BufferPosition containing the position of the offset.</returns>
264+
public BufferPosition GetPositionAtOffset(int bufferOffset)
265+
{
266+
BufferRange bufferRange =
267+
GetRangeBetweenOffsets(
268+
bufferOffset, bufferOffset);
269+
270+
return bufferRange.Start;
271+
}
272+
273+
/// <summary>
274+
/// Calculates the 1-based line and column number range based on
275+
/// the given start and end buffer offsets.
276+
/// </summary>
277+
/// <param name="startOffset">The start offset of the range.</param>
278+
/// <param name="endOffset">The end offset of the range.</param>
279+
/// <returns>A new BufferRange containing the positions in the offset range.</returns>
280+
public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset)
281+
{
282+
bool foundStart = false;
283+
int currentOffset = 0;
284+
int searchedOffset = startOffset;
285+
286+
BufferPosition startPosition = new BufferPosition();
287+
BufferPosition endPosition = startPosition;
288+
289+
int line = 0;
290+
while (line < this.FileLines.Count)
291+
{
292+
if (searchedOffset <= currentOffset + this.FileLines[line].Length)
293+
{
294+
int column = searchedOffset - currentOffset;
295+
296+
// Have we already found the start position?
297+
if (foundStart)
298+
{
299+
// Assign the end position and end the search
300+
endPosition = new BufferPosition(line + 1, column + 1);
301+
break;
302+
}
303+
else
304+
{
305+
startPosition = new BufferPosition(line + 1, column + 1);
306+
307+
// Do we only need to find the start position?
308+
if (startOffset == endOffset)
309+
{
310+
endPosition = startPosition;
311+
break;
312+
}
313+
else
314+
{
315+
// Since the end offset can be on the same line,
316+
// skip the line increment and continue searching
317+
// for the end position
318+
foundStart = true;
319+
searchedOffset = endOffset;
320+
continue;
321+
}
322+
}
323+
}
324+
325+
// Increase the current offset and include newline length
326+
currentOffset += this.FileLines[line].Length + Environment.NewLine.Length;
327+
line++;
328+
}
329+
330+
return new BufferRange(startPosition, endPosition);
331+
}
332+
258333
#endregion
259334

260335
#region Private Methods
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion
7+
{
8+
public class CompleteAttributeValue
9+
{
10+
public static readonly ScriptRegion SourceDetails =
11+
new ScriptRegion
12+
{
13+
File = @"Completion\CompletionExamples.psm1",
14+
StartLineNumber = 16,
15+
StartColumnNumber = 38
16+
};
17+
18+
public static readonly BufferRange ExpectedRange =
19+
new BufferRange(
20+
new BufferPosition(16, 33),
21+
new BufferPosition(16, 38));
22+
}
23+
}
24+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.PowerShell.EditorServices.Test.Shared.Completion
13+
{
14+
public class CompleteFilePath
15+
{
16+
public static readonly ScriptRegion SourceDetails =
17+
new ScriptRegion
18+
{
19+
File = @"Completion\CompletionExamples.psm1",
20+
StartLineNumber = 19,
21+
StartColumnNumber = 25
22+
};
23+
24+
public static readonly BufferRange ExpectedRange =
25+
new BufferRange(
26+
new BufferPosition(19, 15),
27+
new BufferPosition(19, 25));
28+
}
29+
}
30+

0 commit comments

Comments
 (0)