Skip to content

Commit b9cfe1f

Browse files
committed
Merge pull request #36 from rkeithhill/master
Implementation of Find Symbols in Document and Workspace
2 parents e62eb38 + d5200b9 commit b9cfe1f

File tree

13 files changed

+379
-16
lines changed

13 files changed

+379
-16
lines changed

src/PowerShellEditorServices.Host/LanguageServer.cs

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Text.RegularExpressions;
1717
using System.Threading;
1818
using System.Threading.Tasks;
19+
using System.IO;
1920

2021
namespace Microsoft.PowerShell.EditorServices.Host
2122
{
@@ -535,29 +536,119 @@ protected async Task HandleDocumentSymbolRequest(
535536
EditorSession editorSession,
536537
RequestContext<SymbolInformation[], object> requestContext)
537538
{
538-
// TODO: Implement this with Keith's changes.
539+
ScriptFile scriptFile =
540+
editorSession.Workspace.GetFile(
541+
textDocumentIdentifier.Uri);
542+
543+
FindOccurrencesResult foundSymbols =
544+
editorSession.LanguageService.FindSymbolsInFile(
545+
scriptFile);
546+
547+
SymbolInformation[] symbols = null;
539548

540-
// NOTE SymbolInformation.Location's Start/End Position are
541-
// zero-based while the LanguageService APIs are one-based.
542-
// Make sure to subtract line/column positions by 1 when creating
543-
// the result list.
549+
string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath);
550+
551+
if (foundSymbols != null)
552+
{
553+
symbols =
554+
foundSymbols
555+
.FoundOccurrences
556+
.Select(r =>
557+
{
558+
return new SymbolInformation {
559+
ContainerName = containerName,
560+
Kind = GetSymbolKind(r.SymbolType),
561+
Location = new Location {
562+
Uri = new Uri(r.FilePath).AbsolutePath,
563+
Range = GetRangeFromScriptRegion(r.ScriptRegion)
564+
},
565+
Name = GetDecoratedSymbolName(r)
566+
};
567+
})
568+
.ToArray();
569+
}
570+
else
571+
{
572+
symbols = new SymbolInformation[0];
573+
}
544574

545-
await requestContext.SendResult(new SymbolInformation[0]);
575+
await requestContext.SendResult(symbols);
576+
}
577+
578+
private SymbolKind GetSymbolKind(SymbolType symbolType)
579+
{
580+
switch (symbolType)
581+
{
582+
case SymbolType.Configuration:
583+
case SymbolType.Function:
584+
case SymbolType.Workflow:
585+
return SymbolKind.Function;
586+
587+
default:
588+
return SymbolKind.Variable;
589+
}
590+
}
591+
592+
private string GetDecoratedSymbolName(SymbolReference symbolReference)
593+
{
594+
string name = symbolReference.SymbolName;
595+
596+
if (symbolReference.SymbolType == SymbolType.Configuration ||
597+
symbolReference.SymbolType == SymbolType.Function ||
598+
symbolReference.SymbolType == SymbolType.Workflow)
599+
{
600+
name += " { }";
601+
}
602+
603+
return name;
546604
}
547605

548606
protected async Task HandleWorkspaceSymbolRequest(
549607
WorkspaceSymbolParams workspaceSymbolParams,
550608
EditorSession editorSession,
551609
RequestContext<SymbolInformation[], object> requestContext)
552610
{
553-
// TODO: Implement this with Keith's changes
611+
var symbols = new List<SymbolInformation>();
554612

555-
// NOTE SymbolInformation.Location's Start/End Position are
556-
// zero-based while the LanguageService APIs are one-based.
557-
// Make sure to subtract line/column positions by 1 when creating
558-
// the result list.
613+
foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles())
614+
{
615+
FindOccurrencesResult foundSymbols =
616+
editorSession.LanguageService.FindSymbolsInFile(
617+
scriptFile);
559618

560-
await requestContext.SendResult(new SymbolInformation[0]);
619+
// TODO: Need to compute a relative path that is based on common path for all workspace files
620+
string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath);
621+
622+
if (foundSymbols != null)
623+
{
624+
var matchedSymbols =
625+
foundSymbols
626+
.FoundOccurrences
627+
.Where(r => IsQueryMatch(workspaceSymbolParams.Query, r.SymbolName))
628+
.Select(r =>
629+
{
630+
return new SymbolInformation
631+
{
632+
ContainerName = containerName,
633+
Kind = r.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function,
634+
Location = new Location {
635+
Uri = new Uri(r.FilePath).AbsoluteUri,
636+
Range = GetRangeFromScriptRegion(r.ScriptRegion)
637+
},
638+
Name = GetDecoratedSymbolName(r)
639+
};
640+
});
641+
642+
symbols.AddRange(matchedSymbols);
643+
}
644+
}
645+
646+
await requestContext.SendResult(symbols.ToArray());
647+
}
648+
649+
private bool IsQueryMatch(string query, string symbolName)
650+
{
651+
return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
561652
}
562653

563654
protected async Task HandleEvaluateRequest(

src/PowerShellEditorServices/Language/AstOperations.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
131131
scriptAst.Visit(referencesVisitor);
132132

133133
return referencesVisitor.FoundReferences;
134-
135134
}
135+
136136
/// <summary>
137137
/// Finds all references (not including aliases) in a script for the given symbol
138138
/// </summary>
@@ -172,6 +172,19 @@ static public SymbolReference FindDefinitionOfSymbol(
172172
return declarationVisitor.FoundDeclartion;
173173
}
174174

175+
/// <summary>
176+
/// Finds all symbols in a script
177+
/// </summary>
178+
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
179+
/// <returns>A collection of SymbolReference objects</returns>
180+
static public IEnumerable<SymbolReference> FindSymbolsInDocument(Ast scriptAst)
181+
{
182+
FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor();
183+
scriptAst.Visit(findSymbolsVisitor);
184+
185+
return findSymbolsVisitor.SymbolReferences;
186+
}
187+
175188
/// <summary>
176189
/// Finds all files dot sourced in a script
177190
/// </summary>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.Collections.Generic;
7+
using System.Management.Automation.Language;
8+
9+
namespace Microsoft.PowerShell.EditorServices
10+
{
11+
/// <summary>
12+
/// The visitor used to find all the symbols (function and class defs) in the AST.
13+
/// </summary>
14+
internal class FindSymbolsVisitor : AstVisitor2
15+
{
16+
public List<SymbolReference> SymbolReferences { get; private set; }
17+
18+
public FindSymbolsVisitor()
19+
{
20+
this.SymbolReferences = new List<SymbolReference>();
21+
}
22+
23+
/// <summary>
24+
/// Adds each function defintion as a
25+
/// </summary>
26+
/// <param name="functionDefinitionAst">A functionDefinitionAst object in the script's AST</param>
27+
/// <returns>A decision to stop searching if the right symbol was found,
28+
/// or a decision to continue if it wasn't found</returns>
29+
public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)
30+
{
31+
IScriptExtent nameExtent = new ScriptExtent() {
32+
Text = functionDefinitionAst.Name,
33+
StartLineNumber = functionDefinitionAst.Extent.StartLineNumber,
34+
EndLineNumber = functionDefinitionAst.Extent.EndLineNumber,
35+
StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber,
36+
EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber
37+
};
38+
39+
SymbolType symbolType =
40+
functionDefinitionAst.IsWorkflow ?
41+
SymbolType.Workflow : SymbolType.Function;
42+
43+
this.SymbolReferences.Add(
44+
new SymbolReference(
45+
symbolType,
46+
nameExtent));
47+
48+
return AstVisitAction.Continue;
49+
}
50+
51+
/// <summary>
52+
/// Checks to see if this variable expression is the symbol we are looking for.
53+
/// </summary>
54+
/// <param name="variableExpressionAst">A VariableExpressionAst object in the script's AST</param>
55+
/// <returns>A descion to stop searching if the right symbol was found,
56+
/// or a decision to continue if it wasn't found</returns>
57+
public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)
58+
{
59+
if (!IsAssignedAtScriptScope(variableExpressionAst))
60+
{
61+
return AstVisitAction.Continue;
62+
}
63+
64+
this.SymbolReferences.Add(
65+
new SymbolReference(
66+
SymbolType.Variable,
67+
variableExpressionAst.Extent));
68+
69+
return AstVisitAction.Continue;
70+
}
71+
72+
public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst)
73+
{
74+
IScriptExtent nameExtent = new ScriptExtent() {
75+
Text = configurationDefinitionAst.InstanceName.Extent.Text,
76+
StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber,
77+
EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber,
78+
StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber,
79+
EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber
80+
};
81+
82+
this.SymbolReferences.Add(
83+
new SymbolReference(
84+
SymbolType.Configuration,
85+
nameExtent));
86+
87+
return AstVisitAction.Continue;
88+
}
89+
90+
private bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst)
91+
{
92+
Ast parent = variableExpressionAst.Parent;
93+
if (!(parent is AssignmentStatementAst))
94+
{
95+
return false;
96+
}
97+
98+
parent = parent.Parent;
99+
if (parent == null || parent.Parent == null || parent.Parent.Parent == null)
100+
{
101+
return true;
102+
}
103+
104+
return false;
105+
}
106+
}
107+
}

src/PowerShellEditorServices/Language/LanguageService.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,32 @@ public async Task<SymbolDetails> FindSymbolDetailsAtLocation(
198198
return symbolDetails;
199199
}
200200

201+
/// <summary>
202+
/// Finds all the symbols in a file.
203+
/// </summary>
204+
/// <param name="scriptFile">The ScriptFile in which the symbol can be located.</param>
205+
/// <returns></returns>
206+
public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile)
207+
{
208+
Validate.IsNotNull("scriptFile", scriptFile);
209+
210+
IEnumerable<SymbolReference> symbolReferencesinFile =
211+
AstOperations
212+
.FindSymbolsInDocument(scriptFile.ScriptAst)
213+
.Select(
214+
reference => {
215+
reference.SourceLine =
216+
scriptFile.GetLine(reference.ScriptRegion.StartLineNumber);
217+
reference.FilePath = scriptFile.FilePath;
218+
return reference;
219+
});
220+
221+
return
222+
new FindOccurrencesResult {
223+
FoundOccurrences = symbolReferencesinFile
224+
};
225+
}
226+
201227
/// <summary>
202228
/// Finds all the references of a symbol
203229
/// </summary>

src/PowerShellEditorServices/Language/SymbolType.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ public enum SymbolType
2828
/// <summary>
2929
/// The symbol is a parameter
3030
/// </summary>
31-
Parameter
32-
}
31+
Parameter,
32+
33+
/// <summary>
34+
/// The symbol is a DSC configuration
35+
/// </summary>
36+
Configuration,
3337

38+
/// <summary>
39+
/// The symbol is a workflow
40+
/// </summary>
41+
Workflow,
42+
}
3443
}
3544

src/PowerShellEditorServices/PowerShellEditorServices.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<Compile Include="Language\FindOccurrencesResult.cs" />
7575
<Compile Include="Language\FindReferencesResult.cs" />
7676
<Compile Include="Language\FindReferencesVisitor.cs" />
77+
<Compile Include="Language\FindSymbolsVisitor.cs" />
7778
<Compile Include="Language\FindSymbolVisitor.cs" />
7879
<Compile Include="Language\GetDefinitionResult.cs" />
7980
<Compile Include="Language\LanguageService.cs" />

src/PowerShellEditorServices/Workspace/Workspace.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer)
9191
return scriptFile;
9292
}
9393

94+
public ScriptFile[] GetOpenedFiles()
95+
{
96+
var scriptFiles = new ScriptFile[workspaceFiles.Count];
97+
workspaceFiles.Values.CopyTo(scriptFiles, 0);
98+
return scriptFiles;
99+
}
100+
94101
/// <summary>
95102
/// Closes a currently open script file with the given file path.
96103
/// </summary>

test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@
5555
<Compile Include="References\FindsReferencesOnFunctionMultiFileDotSource.cs" />
5656
<Compile Include="References\FindsReferencesOnVariable.cs" />
5757
<Compile Include="SymbolDetails\FindsDetailsForBuiltInCommand.cs" />
58+
<Compile Include="Symbols\FindSymbolsInMultiSymbolFile.cs" />
59+
<Compile Include="Symbols\FindSymbolsInNoSymbolsFile.cs" />
5860
</ItemGroup>
5961
<ItemGroup>
6062
<None Include="Completion\CompletionExamples.psm1">
6163
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6264
</None>
6365
<None Include="Debugging\VariableTest.ps1" />
6466
<None Include="SymbolDetails\SymbolDetails.ps1" />
67+
<None Include="Symbols\MultipleSymbols.ps1" />
68+
<None Include="Symbols\NoSymbols.ps1" />
6569
</ItemGroup>
6670
<ItemGroup>
6771
<ProjectReference Include="..\..\src\PowerShellEditorServices\PowerShellEditorServices.csproj">
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
//
4+
5+
namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols
6+
{
7+
public class FindSymbolsInMultiSymbolFile
8+
{
9+
public static readonly ScriptRegion SourceDetails =
10+
new ScriptRegion {
11+
File = @"Symbols\MultipleSymbols.ps1"
12+
};
13+
}
14+
}

0 commit comments

Comments
 (0)