Skip to content

Commit

Permalink
First working release
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg G committed Jun 26, 2017
1 parent 4e29058 commit bdcd6d6
Show file tree
Hide file tree
Showing 22 changed files with 1,325 additions and 5 deletions.
File renamed without changes.
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,127 @@
# Scry
![Icon](/Scry/Resources/ScryPackage.png)
Visual Studio extension that provides the ability to run C# scripts, giving them access to the current Roslyn workspace.

## Getting started
*Visual Studio 2017+ required.*

### 1. Install extension
Download the VSIX file from the [Releases](../releases), and open any project in Visual Studio.

### 2. Create a Scry script
Right-click on a project or folder, and select `Add > New Item...`. Scroll a bit, and create a new `Scry Script`.

### 3. Run the script
Right-click on the `.csx` script, and select `Run script with Scry`.

## Features
### Imports
By default, the following namespaces (and the matching references) are imported in Scry scripts:
* System
* System.Collections.Generic
* System.Linq
* System.Reflection
* System.Threading.Tasks
* Microsoft.CodeAnalysis
* Microsoft.CodeAnalysis.CSharp
* Microsoft.CodeAnalysis.CSharp.Syntax

### Globals
Properties and methods of the [`Globals`](./Scry/ScriptRunner.cs) class are globally available in Scry scripts. Their definition is (roughly):
```csharp
class Globals
{
// Reference to itself
public Globals Context => this;

// VS Workspace
public VisualStudioWorkspace Workspace { get; }

// Project in which the script file is
public Project Project { get; }

// TextWriter for the generated file
public TextWriter Output { get; }

// Extension of the generated file (default: '.g.cs').
public string Extension { get; set; }

// Path of the script file.
public string ScriptFile { get; }

// Indentation used by WriteIndentation().
public int Indentation { get; set; }

// Alias for 'Indentation'.
public int Indent { get; set; }

// Whether or not WriteLine() methods should automatically
// call WriteIndentation() before writing the given arguments.
public bool AutoWriteIndentation { get; set; }

// Shortcuts to Output.WriteLine
public Globals WriteLine(string format, params object[] args);
public Globals WriteLine(params object[] args);
public Globals Write(string format, params object[] args);
public Globals Write(params object[] args);

// Utility to write multiple using statements.
public Globals WriteUsings(params string[] usings);

// Utilities to write the start and end of a namespace.
public Globals WriteNamespace(string @namespace);
public Globals WriteEnd();

// Change indentation.
public Globals WriteIndentation();
public Globals IncreaseIndentation(int indent = 4);
public Globals DecreaseIndentation(int indent = 4);

// Get root nodes, syntax trees and semantic models.
public SyntaxNode Syntax(string filename);
public CSharpSyntaxTree Tree(string filename);
public SemanticModel Model(string filename);

public IEnumerable<SyntaxNode> Syntaxes { get; }
public IEnumerable<CSharpSyntaxTree> Trees { get; }
public IEnumerable<SemanticModel> Models { get; }
}
```

## Example script
```csharp
AutoWriteIndentation = true;

Context
.WriteUsings("System", "System.Reflection")
.WriteLine()
.WriteNamespace("TestNamespace")

.WriteLine("public static class Files")
.WriteLine('{')
.IncreaseIndentation(4):

foreach (CSharpSyntaxTree tree in Trees)
Context.WriteLine("public static readonly string {0} = \"{1}\";",
Path.GetFileNameWithoutExtension(tree.FilePath ?? ""), tree.FilePath);

Context
.DecreateIndentation(4)
.WriteLine('}')

.WriteEnd();
```

## Nested generated file
You can nest the generated file by copying and pasting the following snippet in the `.csproj` file, and replacing the filenames accordingly.

```xml
<ItemGroup>
<None Update="ScryScript.csx" />
<Compile Update="ScryScript.g.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ScryScript.csx</DependentUpon>
</Compile>
</ItemGroup>
```
134 changes: 134 additions & 0 deletions Scry/Commands/RunProjectScriptsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.IO;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;

namespace Scry
{
/// <summary>
/// Command handler
/// </summary>
internal sealed class RunProjectScriptsCommand
{
/// <summary>
/// Command ID.
/// </summary>
public const int CommandId = 0x0200;

/// <summary>
/// Command menu group (command set GUID).
/// </summary>
public static readonly Guid CommandSet = new Guid("8771fa60-6d1d-4298-b676-64c5eda2b366");

/// <summary>
/// Gets the instance of the command.
/// </summary>
public static RunProjectScriptsCommand Instance { get; private set; }

/// <summary>
/// VS Package that provides this command, not null.
/// </summary>
private readonly Package package;

private string[] LastSelectedScripts;

/// <summary>
/// Initializes a new instance of the <see cref="RunProjectScriptsCommand"/> class.
/// Adds our command handlers for menu (commands must exist in the command table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
private RunProjectScriptsCommand(Package package)
{
this.package = package ?? throw new ArgumentNullException(nameof(package));

if (ServiceProvider.GetService(typeof(IMenuCommandService)) is OleMenuCommandService commandService)
{
var menuCommandID = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(this.MenuItemCallback, menuCommandID);

menuItem.BeforeQueryStatus += BeforeQueryStatus;

commandService.AddCommand(menuItem);
}
}

private void BeforeQueryStatus(object sender, EventArgs e)
{
if (!(sender is OleMenuCommand menuCommand))
return;

menuCommand.Visible = false;
menuCommand.Enabled = false;

DTE2 dte = Package.GetGlobalService(typeof(DTE)) as DTE2;

if (dte == null)
return;

UIHierarchy uih = dte.ToolWindows.SolutionExplorer;
Array uiSelectedItems = uih.SelectedItems as Array;

if (uiSelectedItems == null || uiSelectedItems.Length != 1)
return;

Project proj = (uiSelectedItems.GetValue(0) as UIHierarchyItem)?.Object as Project;

if (proj == null)
return;

Stack<string> scripts = new Stack<string>();

foreach (ProjectItem projItem in proj.ProjectItems)
{
for (short i = 0; i < projItem.FileCount; i++)
{
string fullName = projItem.FileNames[i];

if (Path.GetExtension(fullName) == ".csx")
scripts.Push(fullName);
}
}

if (scripts.Count == 0)
return;

menuCommand.Text = scripts.Count == 1
? "Run script with Scry"
: "Run all scripts with Scry";

menuCommand.Visible = true;
menuCommand.Enabled = true;

LastSelectedScripts = scripts.ToArray();
}

/// <summary>
/// Gets the service provider from the owner package.
/// </summary>
private IServiceProvider ServiceProvider => this.package;

/// <summary>
/// Initializes the singleton instance of the command.
/// </summary>
/// <param name="package">Owner package, not null.</param>
public static void Initialize(Package package)
{
Instance = new RunProjectScriptsCommand(package);
}

/// <summary>
/// This function is the callback used to execute the command when the menu item is clicked.
/// See the constructor to see how the menu item is associated with this function using
/// OleMenuCommandService service and MenuCommand class.
/// </summary>
/// <param name="sender">Event sender.</param>
/// <param name="e">Event args.</param>
private async void MenuItemCallback(object sender, EventArgs e)
{
await ScriptRunner.RunScripts(LastSelectedScripts);
}
}
}
Loading

0 comments on commit bdcd6d6

Please sign in to comment.