Skip to content

Commit 6807e9d

Browse files
author
Kapil Borle
authored
Merge pull request #370 from PowerShell/kapilmb/format-on-type
Enable formatOnType feature
2 parents 6f3fedd + 6dd55d3 commit 6807e9d

File tree

4 files changed

+253
-0
lines changed

4 files changed

+253
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
7+
8+
namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
9+
{
10+
/// <summary>
11+
/// Class to encapsulate the request type.
12+
/// </summary>
13+
class ScriptRegionRequest
14+
{
15+
public static readonly
16+
RequestType<ScriptRegionRequestParams, ScriptRegionRequestResult> Type =
17+
RequestType<ScriptRegionRequestParams, ScriptRegionRequestResult>.Create("powerShell/getScriptRegion");
18+
}
19+
20+
/// <summary>
21+
/// Class to encapsulate the request parameters.
22+
/// </summary>
23+
class ScriptRegionRequestParams
24+
{
25+
/// <summary>
26+
/// Path of the file for which the formatting region is requested.
27+
/// </summary>
28+
public string FileUri;
29+
30+
/// <summary>
31+
/// Hint character.
32+
/// </summary>
33+
public string Character;
34+
35+
/// <summary>
36+
/// 1-based line number of the character.
37+
/// </summary>
38+
public int Line;
39+
40+
/// <summary>
41+
/// 1-based column number of the character.
42+
/// </summary>
43+
public int Column;
44+
}
45+
46+
/// <summary>
47+
/// Class to encapsulate the result of ScriptRegionRequest.
48+
/// </summary>
49+
class ScriptRegionRequestResult
50+
{
51+
/// <summary>
52+
/// A region in the script that encapsulates the given character/position which is suitable
53+
/// for formatting
54+
/// </summary>
55+
public ScriptRegion scriptRegion;
56+
}
57+
}

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+43
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ protected override void Initialize()
125125
this.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequest);
126126

127127
this.SetRequestHandler(ScriptFileMarkersRequest.Type, this.HandleScriptFileMarkersRequest);
128+
this.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequest);
128129

129130
this.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest);
130131

@@ -235,6 +236,48 @@ await RunScriptDiagnostics(
235236
await sendresult;
236237
}
237238

239+
private async Task HandleGetFormatScriptRegionRequest(
240+
ScriptRegionRequestParams requestParams,
241+
RequestContext<ScriptRegionRequestResult> requestContext)
242+
{
243+
var scriptFile = this.editorSession.Workspace.GetFile(requestParams.FileUri);
244+
var lineNumber = requestParams.Line;
245+
var columnNumber = requestParams.Column;
246+
ScriptRegion scriptRegion = null;
247+
248+
switch (requestParams.Character)
249+
{
250+
case "\n":
251+
// find the smallest statement ast that occupies
252+
// the element before \n or \r\n and return the extent.
253+
--lineNumber; // vscode sends the next line when pressed enter
254+
var line = scriptFile.GetLine(lineNumber);
255+
if (!String.IsNullOrEmpty(line))
256+
{
257+
scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion(
258+
scriptFile,
259+
lineNumber,
260+
line.Length);
261+
}
262+
break;
263+
264+
case "}":
265+
scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion(
266+
scriptFile,
267+
lineNumber,
268+
columnNumber);
269+
break;
270+
271+
default:
272+
break;
273+
}
274+
275+
await requestContext.SendResult(new ScriptRegionRequestResult
276+
{
277+
scriptRegion = scriptRegion
278+
});
279+
}
280+
238281
private async Task HandleScriptFileMarkersRequest(
239282
ScriptFileMarkerRequestParams requestParams,
240283
RequestContext<ScriptFileMarkerRequestResultParams> requestContext)

src/PowerShellEditorServices/Language/LanguageService.cs

+33
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,28 @@ await CommandHelpers.GetCommandInfo(
450450
}
451451
}
452452

453+
/// <summary>
454+
/// Gets the smallest statment ast that contains the given script position as
455+
/// indicated by lineNumber and columnNumber parameters.
456+
/// </summary>
457+
/// <param name="scriptFile">Open script file.</param>
458+
/// <param name="lineNumber">1-based line number of the position.</param>
459+
/// <param name="columnNumber">1-based column number of the position.</param>
460+
/// <returns></returns>
461+
public ScriptRegion FindSmallestStatementAstRegion(
462+
ScriptFile scriptFile,
463+
int lineNumber,
464+
int columnNumber)
465+
{
466+
var ast = FindSmallestStatementAst(scriptFile, lineNumber, columnNumber);
467+
if (ast == null)
468+
{
469+
return null;
470+
}
471+
472+
return ScriptRegion.Create(ast.Extent);
473+
}
474+
453475
#endregion
454476

455477
#region Private Fields
@@ -569,6 +591,17 @@ private SymbolReference FindDeclarationForBuiltinCommand(
569591
return foundDefinition;
570592
}
571593

594+
private Ast FindSmallestStatementAst(ScriptFile scriptFile, int lineNumber, int columnNumber)
595+
{
596+
var asts = scriptFile.ScriptAst.FindAll(ast =>
597+
{
598+
return ast is StatementAst && ast.Extent.Contains(lineNumber, columnNumber);
599+
}, true);
600+
601+
// Find ast with the smallest extent
602+
return asts.MinElement((astX, astY) => astX.Extent.ExtentWidthComparer(astY.Extent));
603+
}
604+
572605
#endregion
573606
}
574607
}

src/PowerShellEditorServices/Utility/Extensions.cs

+120
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
//
55

66
using System;
7+
using System.Linq;
8+
using System.Collections.Generic;
9+
using System.Management.Automation.Language;
710

811
namespace Microsoft.PowerShell.EditorServices.Utility
912
{
@@ -30,5 +33,122 @@ public static string SafeToString(this object obj)
3033

3134
return str;
3235
}
36+
37+
/// <summary>
38+
/// Get the maximum of the elements from the given enumerable.
39+
/// </summary>
40+
/// <typeparam name="T">Type of object for which the enumerable is defined.</typeparam>
41+
/// <param name="elements">An enumerable object of type T</param>
42+
/// <param name="comparer">A comparer for ordering elements of type T. The comparer should handle null values.</param>
43+
/// <returns>An object of type T. If the enumerable is empty or has all null elements, then the method returns null.</returns>
44+
public static T MaxElement<T>(this IEnumerable<T> elements, Func<T,T,int> comparer) where T:class
45+
{
46+
if (elements == null)
47+
{
48+
throw new ArgumentNullException(nameof(elements));
49+
}
50+
51+
if (comparer == null)
52+
{
53+
throw new ArgumentNullException(nameof(comparer));
54+
}
55+
56+
if (!elements.Any())
57+
{
58+
return null;
59+
}
60+
61+
var maxElement = elements.First();
62+
foreach(var element in elements.Skip(1))
63+
{
64+
if (element != null && comparer(element, maxElement) > 0)
65+
{
66+
maxElement = element;
67+
}
68+
}
69+
70+
return maxElement;
71+
}
72+
73+
/// <summary>
74+
/// Get the minimum of the elements from the given enumerable.
75+
/// </summary>
76+
/// <typeparam name="T">Type of object for which the enumerable is defined.</typeparam>
77+
/// <param name="elements">An enumerable object of type T</param>
78+
/// <param name="comparer">A comparer for ordering elements of type T. The comparer should handle null values.</param>
79+
/// <returns>An object of type T. If the enumerable is empty or has all null elements, then the method returns null.</returns>
80+
public static T MinElement<T>(this IEnumerable<T> elements, Func<T, T, int> comparer) where T : class
81+
{
82+
return MaxElement<T>(elements, (elementX, elementY) => -1 * comparer(elementX, elementY));
83+
}
84+
85+
/// <summary>
86+
/// Compare extents with respect to their widths.
87+
///
88+
/// Width of an extent is defined as the difference between its EndOffset and StartOffest properties.
89+
/// </summary>
90+
/// <param name="extentX">Extent of type IScriptExtent.</param>
91+
/// <param name="extentY">Extent of type IScriptExtent.</param>
92+
/// <returns>0 if extentX and extentY are equal in width. 1 if width of extent X is greater than that of extent Y. Otherwise, -1.</returns>
93+
public static int ExtentWidthComparer(this IScriptExtent extentX, IScriptExtent extentY)
94+
{
95+
96+
if (extentX == null && extentY == null)
97+
{
98+
return 0;
99+
}
100+
101+
if (extentX != null && extentY == null)
102+
{
103+
return 1;
104+
}
105+
106+
if (extentX == null)
107+
{
108+
return -1;
109+
}
110+
111+
var extentWidthX = extentX.EndOffset - extentX.StartOffset;
112+
var extentWidthY = extentY.EndOffset - extentY.StartOffset;
113+
if (extentWidthX > extentWidthY)
114+
{
115+
return 1;
116+
}
117+
else if (extentWidthX < extentWidthY)
118+
{
119+
return -1;
120+
}
121+
else
122+
{
123+
return 0;
124+
}
125+
}
126+
127+
/// <summary>
128+
/// Check if the given coordinates are wholly contained in the instance's extent.
129+
/// </summary>
130+
/// <param name="scriptExtent">Extent of type IScriptExtent.</param>
131+
/// <param name="line">1-based line number.</param>
132+
/// <param name="column">1-based column number</param>
133+
/// <returns>True if the coordinates are wholly contained in the instance's extent, otherwise, false.</returns>
134+
public static bool Contains(this IScriptExtent scriptExtent, int line, int column)
135+
{
136+
if (scriptExtent.StartLineNumber > line || scriptExtent.EndLineNumber < line)
137+
{
138+
return false;
139+
}
140+
141+
if (scriptExtent.StartLineNumber == line)
142+
{
143+
return scriptExtent.StartColumnNumber <= column;
144+
}
145+
146+
if (scriptExtent.EndLineNumber == line)
147+
{
148+
return scriptExtent.EndColumnNumber >= column;
149+
}
150+
151+
return true;
152+
}
33153
}
34154
}

0 commit comments

Comments
 (0)