Lean Solution Explorer for C#

Visual Studio's Solution Explorer for VS Code: the solution, its folders, its projects and their
dependencies, read straight from the .sln or .slnx file. It is for people working in a C# solution in
VS Code who want the tree, the NuGet window and the Run and Debug buttons they already know, without a
language server or an MSBuild evaluation between them and the file list.
Click the Solution Explorer logo in the activity bar for four sections: Solutions, laid out the way
Visual Studio's Solution Explorer lays it out - solution folders, projects, dependencies, files - then
Bookmarks, TODO and Run Configurations.
What it does today
- Every solution in the workspace loads. Multi-root workspaces and repositories with several
.sln files
all show up side by side. .slnx (the .NET 9+ XML solution format) is supported alongside classic .sln.
- Solution structure, not just folders. Solution folders, their nesting, and
SolutionItems files are
rendered from the solution file itself. Solution folders sort above projects, and both the solution and
each solution folder show how many projects sit below them.
- Projects. SDK-style projects list what is actually on disk (minus
bin, obj, .vs, .git,
node_modules); legacy projects list their explicit <Compile Include="..."/> items folded into folders.
- Dependencies. Target frameworks, project references and package references with their versions.
- Edit. Every file and folder carries an
Edit submenu: Cut, Copy, Paste, Delete and Rename.
- Project references. Add and remove them from a project with a tick list that says which projects are
already within reach through another reference, and which would close a loop.
- Reference diagrams. Right-click a solution for two views of how its projects depend on each other, and
two more of which NuGet packages they pull in and at which versions. All four carry a filter bar.
- Endpoints Explorer. Every attribute-routed controller action and minimal API in a solution, opened as a
form the way Swagger opens one: each value the action binds, with its C# type, and a body example shaped
from the DTO it takes. Send the request from the tab.
- Dead Code Explorer. The types nothing else in the solution mentions, grouped by category or by
confidence, with the reason a finding was held back where there is one.
- NuGet Package Manager. A tool window in the shape JetBrains Rider gives it: installed packages with the
updates gathered at the top, a Discover search across your configured feeds, and every change run as a
dotnet command in a terminal you can read.
- NuGet Health. A solution-wide report beside it: which packages sit at different versions in different
projects, which have known vulnerabilities, which are deprecated, and where a transitive dependency came
from - with bulk fixes and a Central Package Management migration, each previewed before it writes.
- Bookmarks. Mark a folder, a file, or the method under the cursor, and they come back with the workspace.
- TODO. Every
TODO, FIXME and HACK comment in the workspace, grouped by file.
- Run Configurations. A startup project, the launch profiles it declares, and Run and Debug on the
section's title bar.
- Build, Clean, Rebuild and Publish. On the context menu of any solution or project, and in the Run
Configurations section for the startup project. They run as VS Code tasks with the
$msCompile problem
matcher, so every MSBuild error lands in the Problems panel where you can click it. Publish asks what to
publish - configuration, framework, runtime, deployment mode, output folder - and remembers the answers.
- Tree line colour. The tree's indent guides can take a colour from the palette the extension's own icons
are drawn in, or any hex colour you like. VS Code draws every tree in the window from one pair of colours,
so this is mirrored into your
workbench.colorCustomizations and the File Explorer, Search and Debug trees
get the same lines.
- Single click opens the file, in preview mode by default.
- Auto reveal. The active editor's file is selected in the tree, expanding only the branch that leads to it.
- Live updates. Adding or deleting a file redraws just the affected folder; editing a
.csproj reloads
just that project.
- Add New Project. Right-click a solution or a solution folder and the new project is created there -
the template list comes from
dotnet new, and the project is filed in the solution folder you clicked.
- Add New Item. Right-click a project or a folder inside one for a Class, Interface, Record, Struct or
Enum, written into that folder with the namespace the folder implies. Add Directory and Add Scratch
File sit below it for a plain folder or a file of any kind.
Requirements
VS Code 1.90 or later. That is the whole requirement for the tree: it has no runtime dependencies, needs
no language server, and never starts a process to draw itself, so it works in a workspace with no .NET
installed at all.
Two things are needed only by the features that reach outside the extension, and each one degrades on its own
rather than taking anything else with it:
| Needed |
For |
The .NET SDK on PATH |
Add New Project...; the NuGet install, update and remove operations; the NuGet Health report; Build, Clean, Rebuild, Publish, Run and Debug |
The C# extension, ms-dotnettools.csharp |
The coreclr debugger behind Debug, and the symbol used when bookmarking the method under the cursor |
C# Dev Kit, ms-dotnettools.csdevkit (optional) |
The .NET: New Project... fallback Add New Project... offers when no SDK is on PATH |
Without the SDK, the tree, the four diagrams, Bookmarks, TODO, the Endpoints Explorer, the Dead Code
Explorer, Add New Item... and the NuGet Packages tab all still work. Add New Project... offers C# Dev
Kit's .NET: New Project... instead, and NuGet Health falls back to the drift analysis, which is
computable from the project files alone.
Without the C# extension, Run is unaffected - it is dotnet run and nothing else. Only Debug needs
the debugger, and a bookmark with no symbol to name falls back to marking the line, which is never wrong.
The dotnet-backed actions also need a trusted workspace. In a restricted workspace the tree, the diagrams,
the Endpoints and Dead Code explorers and the NuGet Packages tab all still open, and none of them starts a
process. NuGet Health drops to the drift analysis it can compute from the project files; the three
dotnet list package runs behind the vulnerability, deprecation and outdated columns are what needs the
trust, and they are not made.
The extension walks the file system directly, so it needs a real one: a local folder, or a remote one over
SSH, WSL or a dev container. It is disabled in virtual workspaces such as vscode.dev and github.dev,
where there is no file system to walk.
Installation
From the Marketplace: search for C# Solution Explorer in the Extensions view, or open the
listing
and press Install.
From Quick Open (Ctrl+P, or Cmd+P on macOS):
ext install taosif-bin-murtaza.vscode-csharp-solution-explorer
From a locally built .vsix - npm run package writes one into the repository root:
code --install-extension vscode-csharp-solution-explorer-0.2.0.vsix
The same file can be installed from the Extensions view menu, under Install from VSIX....
Quick Start
- Open a folder holding a
.sln, .slnx or a project file. Every solution in the workspace is found,
including a multi-root one.
- Click the Solution Explorer logo in the activity bar. Solutions opens on what was found; expand one
and it is parsed then, not before.
- Expand a project for its files, and Dependencies for its target frameworks, project references and
packages.
- Right-click a project and choose Set as Startup Project.... With a single solution in the workspace
one is chosen for you the first time it is needed.
- Press Run - the green triangle in the top right of the editor, and on the Run Configurations title
bar - or Debug beside it. The configuration in force is on the Configuration row; click it to
change it.
From there: right-click a solution for Manage NuGet Packages..., the reference and package diagrams, the
Endpoints Explorer and the Dead Code Explorer.
Why it stays fast
The extension does no MSBuild evaluation and has no runtime dependencies. Reading the tree never starts a
process; only the dotnet commands - Add New Project..., the NuGet operations, Build, Clean, Rebuild,
Publish, Run and Debug - shell out, and only when you ask them to.
| Decision |
Effect |
| Activates only in a workspace holding a solution or project file |
A repository with no .NET in it loads nothing |
| The view never waits on a workspace search |
Solutions found last time paint immediately; the scan runs behind them |
| Breadth-first scan that reports as it goes |
Solutions surface at the top of the tree first, not when the walk ends |
| Several solutions start collapsed |
Nothing is parsed until you click the one you want |
| Solution files parsed on first expand |
Expanding a solution is the first thing the tree parses |
| Children resolved per node and cached |
A 300-project solution reads 0 projects until you expand one |
One readdir per folder, no stat except for symlinks |
Folder expansion is a single syscall pass |
| Project models cached by mtime + size |
Re-expanding a project re-parses nothing |
| Project kind read from the file's first 4 KB, after its solution is drawn |
Web projects get their own icon without a read-everything pass |
| File watchers created after the scan, not at activation |
The first paint is never behind watcher setup |
| Targeted invalidation via a directory index |
A new file redraws one folder, not the tree |
| Debounced watcher (100 ms), create/delete only |
Typing in a file never touches the tree |
| The TODO sweep waits until that section is opened |
Never paid by anyone who does not use it |
| Saving a file re-reads that one file, not the workspace |
The TODO list stays current for free |
On a four-solution, 47-project workspace the first solutions appear 4 ms into the scan and the whole
workspace is walked in 46 ms - and on every later open the remembered solutions are on screen before the
scan even starts.
Settings
| Setting |
Default |
Meaning |
csharpSolutionExplorer.autoReveal |
true |
Select the active editor's file in the tree |
csharpSolutionExplorer.showDependencies |
true |
Show the Dependencies node under each project |
csharpSolutionExplorer.showProjectsWithoutSolution |
true |
Fall back to project roots when no solution exists |
csharpSolutionExplorer.excludedFolders |
["bin", "obj", ".vs", ".git", "node_modules"] |
Folder names skipped by the scan and hidden inside projects |
csharpSolutionExplorer.maxScanDepth |
10 |
Directory levels searched below each workspace folder |
csharpSolutionExplorer.todoTags |
["TODO", "FIXME", "HACK"] |
Comment tags collected into the TODO section |
csharpSolutionExplorer.todoExtensions |
[".cs", ".razor", ".cshtml"] |
File extensions searched for those tags |
csharpSolutionExplorer.openPreview |
true |
Open files in preview mode on single click |
csharpSolutionExplorer.buildConfiguration |
Debug |
Configuration used by Build, Clean, Rebuild, Run and Debug until one is chosen in the view |
csharpSolutionExplorer.publishConfiguration |
Release |
Configuration the Publish picker starts on |
csharpSolutionExplorer.buildArguments |
[] |
Extra arguments appended to dotnet build, one per entry. Clean is left alone |
csharpSolutionExplorer.publishArguments |
[] |
Extra arguments appended to dotnet publish, one per entry |
csharpSolutionExplorer.treeLineColor |
"" |
Colour of the tree's indent guides as a hex colour, mirrored into workbench.colorCustomizations. Empty leaves the colour theme's own |
Commands
All 42, and where each one is. Most belong to a node, so they are on a context menu and hidden from the
command palette - offering Rename... with nothing selected would mean nothing.
The tree
| Command |
Where |
Refresh |
Solutions title bar, command palette |
Reveal Active File |
Solutions title bar, command palette |
Tree Line Colour... |
Solutions view menu, command palette |
Open File |
The click that opens a file |
Creating
| Command |
Where |
Add New Project... |
Solution, solution folder |
Add New Item... |
Project, folder |
Add Directory... |
Project, folder |
Add Scratch File... |
Project, folder |
References, diagrams and readers
| Command |
Where |
Add Project Reference... |
Project |
Remove Project Reference... |
Project |
Project Reference Diagram |
Solution, solution folder, project |
Package Reference Diagram |
Solution, solution folder, project |
Endpoints Explorer |
Solution, solution folder, project |
Dead Code Explorer |
Solution, solution folder, project |
Manage NuGet Packages... |
Solution, solution folder, project, any Dependencies node, Solutions title bar, command palette |
Bookmarks and TODO
| Command |
Where |
Add Bookmark |
File, folder, the editor's context menu, command palette |
Remove Bookmark |
A bookmark, inline and on its context menu |
Remove All Bookmarks |
Bookmarks title bar, command palette |
Open Bookmark |
The click that opens a bookmark |
Search Again |
TODO title bar, command palette |
Open TODO |
The click that opens a TODO |
Files and folders
| Command |
Where |
Reveal in File Explorer |
File, folder, project, solution |
Open in Integrated Terminal |
Folder, project, solution |
Copy Path |
File, folder, project, solution |
Copy Relative Path |
File, folder, project, solution |
Find in Folder... |
Folder, project, solution |
Cut Copy Paste Delete Rename... |
The Edit submenu on any file or folder. Paste appears only once something has been cut or copied |
Run and build
| Command |
Where |
Build |
Solution, project, Run Configurations title bar and Actions, command palette |
Rebuild |
Solution, project, Run Configurations Actions, command palette |
Clean |
Solution, project, Run Configurations Actions, command palette |
Publish... |
Solution, project, Run Configurations Actions, command palette |
Select Build Configuration... |
The Configuration row, command palette |
Run |
The editor's title bar, Run Configurations title bar, command palette |
Debug |
The editor's title bar, Run Configurations title bar, command palette |
Set as Startup Project... |
Project, the Startup Project row, command palette |
Use Launch Profile |
The click on a launch profile row |
Edit launchSettings.json |
Run Configurations Actions, command palette |
Refresh Run Configurations |
Run Configurations title bar, command palette |
Everything that runs the .NET SDK - Add New Project..., the NuGet operations, and Build, Clean, Rebuild,
Publish, Run and Debug - is offered in a trusted workspace only.
Add New Project...
On the context menu of a solution or solution folder. It picks a template from dotnet new list --type project, asks for a name, runs dotnet new into the folder you right-clicked, then dotnet sln add --solution-folder so the project lands in that same solution folder. It needs the .NET SDK on PATH and a
trusted workspace; without an SDK it offers C# Dev Kit's .NET: New Project... instead.
Solution folders are virtual, so the directory is inferred: the matching path below the solution when one
exists (src -> <solution>/src), otherwise the directory the projects already filed there share, otherwise
the solution's own directory.
Add New Item...
On the context menu of a project or any folder inside one. Pick Class, Interface, Record, Struct or Enum,
give it a name, and the file is written straight to disk - no SDK, no process, no wait.
- Namespace is
<RootNamespace> (or the project file's name) plus the folders between the project and
the file, folded into identifiers the way MSBuild folds them: My Folder/2Do -> My_Folder._2Do.
- File-scoped or block-scoped follows
csharp_style_namespace_declarations from the nearest
.editorconfig. Without one it is file-scoped only when every target framework is .NET 6 or later, since
that is where C# 10 begins.
- Legacy projects get a matching
<Compile Include="..."/> next to the last one in the project file;
commented-out items are skipped, and a project whose shape is unclear is reported rather than rewritten.
- Types are declared
internal, which is what Visual Studio writes.
Add Directory... and Add Scratch File...
Directly below Add New Item..., on the same nodes.
Add Directory... makes a folder; Add Scratch File... makes an empty file of whatever kind you name -
notes.md, Dockerfile and .gitignore all work, since the extension is taken verbatim. Both accept a
sub-path, so Models/Dto creates two levels at once, and neither will climb out of the folder you clicked.
In a legacy project the new file is registered too: <Compile> for .cs, <None> for everything else. An
empty folder has no representation in a project that lists its files one by one, so that case is reported
rather than silently doing nothing.
Edit
An Edit submenu on every file and folder: Cut, Copy, Paste, Delete, Rename....
Paste only appears once something has been cut or copied, and refuses to paste a folder into itself. A name
already taken gets copy appended, then copy 2. A cut across volumes falls back to a copy and a delete,
which a plain rename cannot do.
Rename goes through a workspace edit rather than a bare rename, so language extensions get their chance to
follow the file and update namespaces and references. Delete confirms first and goes to the recycle bin,
falling back to a permanent delete only where there is no recycle bin.
Legacy projects keep up throughout: renaming or moving a file rewrites its item, deleting removes it, and
doing either to a folder carries every item below it along. Wildcard includes and commented-out items are
left alone.
Adding and removing project references
Add Project Reference... and Remove Project Reference... on a project. Both open a tick list, so several
go in or out at once, and the project file is edited in a single pass.
A project reference carries its own references with it - reference B, and everything B references arrives
too. Only the projects worth adding get a checkbox; the rest are shown with the reason and no way to tick
them, since quick pick draws a separator without one:
| Row |
Meaning |
ticked box, brings N more with it |
how much else arrives along with this one |
no box, arrives through X |
already in reach, so there is nothing to add |
no box, already references this project |
referencing it back would close a loop |
would still arrive through X |
on removal - another reference still carries it, so nothing is lost |
New references join the item group the existing ones live in; a group left empty is taken out with them.
Commented-out references are left alone.
Project reference diagrams
On the context menu of a solution, a solution folder, or a project. It reads every <ProjectReference> in
scope and opens two tabs, because no one diagram answers everything about a dependency graph.
Reference Graph - a layered node-link drawing. A project sits one layer below everything that references
it, so applications end up along the top and the shared libraries settle at the bottom; the number of layers
is the depth of the stack. Cycles are found first and the edges that close them are drawn dashed and red, so
the rest can still be ranked. Within a layer, nodes are ordered by repeated median sweeps, the standard cheap
way to pull most of the crossings out. Hovering a project dims everything it does not touch; clicking opens
it. Drag to pan, scroll to zoom.
This is the reading people expect of a dependency diagram, and it is the better of the two up to something
like a hundred projects. Past that the edges become a hairball.
Reference Matrix - the same edges as a grid, which never becomes a hairball however many projects there
are. Rows and columns are in layer order, so an acyclic solution fills only the lower triangle and any mark
above the diagonal is a cycle. A dense row is a project that depends on everything; a dense column is one
everything depends on. Fan-in and fan-out totals run along the edges, and the number beside each row is its
layer.
Both views count what they found and say whether the graph is acyclic. References to projects outside the
solution are kept and marked rather than dropped.
Both carry the same filter bar: by name, by Kind - library, web, or referenced from outside the solution -
and by In a cycle, which keeps only the projects sitting on a reference that closes one. + neighbours
widens a match back out to what it references and what references it, which is usually the question being
asked of one project. The graph hides what is filtered out and refits around what is left; the matrix drops
the row and the column together and re-counts, since a fan-out still counting projects the filter had taken
away would be a number that lies.
Package reference diagrams
Package Reference Diagram, on the same nodes, reads every <PackageReference> in scope and opens two more
tabs. Package references make a bipartite graph - projects on one side, packages on the other, and no
package depends on another, because a project file states only what it asks for directly. That different
shape wants different drawings.
Package Graph - two columns, projects on the left with the number of packages each pulls in, packages on
the right with the number of projects using each. Laid out in rows a solution's worth of packages runs
thousands of pixels wide; in columns it reads top to bottom like a list. A few barycentre sweeps keep the
edges between the columns from becoming a mesh. Hover either column to isolate what it touches.
Package Matrix - packages as rows, projects as columns, ordered so the widely shared packages are at the
top. A package's versions are spelled out beside its name, which is the room the row header has and the
column does not. A package used at more than one version turns red, and its cells name the version each
project is on - the answer worth having, and the one nothing else in the tree will tell you.
Both filter by name across the two columns, by a single project, and by Conflicting or Shared -
packages used at more than one version, and packages used by two or more projects. The package graph closes
its two columns up around what is left and redraws every line between the new positions, so a filtered
drawing looks like a drawing of that size rather than a full one with holes in it.
Endpoints Explorer
Endpoints Explorer, on a solution, a solution folder or a project. It reads every attribute-routed
controller action and every minimal API in scope and opens them as a list you can send a request from.
Opening a row gives you a form, the shape Swagger gives one. Every value the action binds is listed - path,
query, header, form - each with its C# type, a red asterisk where it is required, a picker where the type is
an enum or a bool, and the default from the signature as the placeholder, so an empty box means the
server's own default applies. [FromQuery] Filter filter is listed as one input per property, flattened the
way the binder reads it back, and [FromHeader(Name = "X-Tenant")] goes out as that header rather than as a
query key. The URL under the form is assembled as you type, with any token still unfilled shown in red; Send
will not go until the required ones are filled, and marks the boxes that are missing.
A body has an example in it before you touch it, shaped from the DTO the action takes: every property of
every nested record and list, with a placeholder of the right JSON type, and Example and Format
buttons beside Send. Records, positional records, classes, structs and enums are read across the whole
solution, so a type declared in another project still shapes the payload. [FromForm] values are sent
url-encoded instead of as JSON.
Nothing is compiled: the signature, the route template and the DTOs are read as text, on the same pass over
the files the explorer already made, and the base URLs come from launchSettings.json. Where the reader
cannot work something out, the row keeps an extra query box to type it in by hand.
Dead Code Explorer
Dead Code Explorer, on the same nodes. It counts what every declared type is mentioned by and reports the
ones nothing mentions.
The counting is textual, and that sets the shape of the whole thing. A reference count cannot see reflection,
dependency injection by convention, or a caller in another solution, so a finding is a lead and never a
verdict - the page says so in its banner and against every row that has a reason to doubt it. The analysis is
deliberately biased towards saying nothing: every route by which something could be reached without being
named - a base list, a string literal, an attribute, a controller the framework routes to - is recorded and
used to hold a finding back or mark it down, never to push one forward.
| Confidence |
Meaning |
| high |
Nothing suggests anything else can reach it |
| medium |
It could be reached without being named - it is public, it carries an attribute, or it is an interface only its implementations know |
| low |
Something almost certainly reaches it another way: a controller the framework routes to, or a name that appears in a string literal |
Findings group by category - Controller, Service, DTO, Interface, Enum, Class - or by confidence, by project,
or not at all, and the bar filters by name, by project and by confidence. A row says which project and file
it is in, how many types derive from it, and whether it is the only declaration in its file, which is the
difference between deleting a type and deleting a file. Clicking a name opens it.
A type's own file is discounted, since a class used only by itself is exactly what this is looking for.
Generated files are not reported, and test projects are read for their mentions even where their own
declarations are not - a mention is a mention wherever it comes from.
NuGet Package Manager
Manage NuGet Packages... on a solution, a solution folder, a project or anything under Dependencies -
and on the Solutions title bar. Right-clicking one package under Dependencies opens the window already on
it.
Two tabs, laid out the way Rider's NuGet tool window is laid out - the list on the left, the selected package
on the right, and a divider you can drag between them.
Packages
Installed opens on what your project files already say, so the list is on screen before a single request
goes out. Above it sits Packages to Update in Solution: N, filled in as the answers arrive from the feed:
each row carries the version you are on and the version on offer, and the checkboxes plus the group's
Update button move as many of them as you like in one go. Below that, the packages that are already
current. Discover searches your feeds instead, with downloads, a reserved-prefix tick and a mark against
anything you already reference.
On the right: a version dropdown carrying every published version, Info (description, authors, licence,
publication date, links to the project and the gallery), Frameworks and Dependencies read from the
registration leaf for the exact version selected, and then the projects in scope. Each project row installs
with + or removes with the bin - so one package can go into one project without touching the rest.
Which feeds are searched comes from the whole NuGet.Config chain that applies to the solution - <clear/>,
<disabledPackageSources> and the nearest file winning are all honoured - and the dropdown lists the ones
that can actually be queried.
Health
Deliberately not another package browser. It is a solution-wide report answering the four questions that get
harder as a solution grows.
Drift is the quiet one. Three projects on three versions of Microsoft.Extensions.Logging compiles fine
and then produces a binding surprise at run time, and nothing else in the tooling says so. It is a matrix -
package per row, project per column - so a row whose versions do not match is wrong at a glance, with nothing
to read.
Vulnerable and deprecated packages sort to the top with a severity badge. Transitive origin
arrives on expand: the direct reference that pulled the package in, which is the actual question when a
transitive dependency is flagged. That chain is read out of obj/project.assets.json, the only file that
knows it.
Three bulk actions and a migration sit along the bottom, each showing exactly what it would do before it does
it:
| Action |
What it does |
| Fix vulnerabilities |
Moves every vulnerable package to the newest published version. A vulnerable transitive package is named, not touched - it says which direct reference to update instead. |
| Unify drifted rows |
Brings every project on to the highest version any of them already uses. Also on a single row, from its expander. |
| Update patch versions |
Only where major and minor already match. |
| Migrate to Central Package Management |
Lifts every version into Directory.Packages.props and drops the Version attribute from each project. |
The migration is the risky one, so it is built to be refused. It will not start on a dirty working tree - a
clean tree is the undo. It shows a real diff of every file, including the props file it would create, before
anything is written. It leaves alone, and reports, everything the props file cannot express on its own: a
reference in a conditional ItemGroup, a VersionOverride, a floating 8.0.*, a version that comes from an
MSBuild property, an Update rather than an Include. PrivateAssets and every other metadata stay exactly
as they were. And once applied, Undo migration puts every file back.
Not all drift is a defect - a test project on a newer analyser is fine - so any package can be muted from
its expander. Muted packages stop counting, and the mute is remembered with the workspace.
How a change is made
Nothing here edits your project file, the CPM migration aside. Every change is dotnet add package or
dotnet remove package, sent to a terminal named NuGet that opens at the solution and is reused - so you
see the command, you see the restore, and you see it fail if it fails. Paths are relative to that terminal,
which keeps the transcript to something you could have typed yourself. More than three projects at once, or
removing from several, asks first. When the SDK finishes, the project file it wrote is picked up by a watcher
and the list redraws itself.
Central Package Management is understood throughout: a project that names a package with no version is
matched against Directory.Packages.props, the version it contributes is shown, and the row is marked
central.
What it costs
The Packages tab opens on cached project files and no network at all. The update check that follows is one
static index.json per package from the feed's package base address - the cheapest question a feed answers -
eight in flight at a time, each posted into the list as it lands, so fifty packages fill in progressively
instead of waiting on the slowest one. Answers are cached with a TTL (feed index 30 min, versions 5 min,
search 2 min) and de-duplicated while in flight, so typing in the search box never sends a request that is
already on its way back. Searching is debounced and a superseded search is dropped rather than rendered.
The Health tab costs three dotnet list package invocations, so it does not run until the tab is opened, and
they go out together rather than one after another. Everything they cannot answer degrades rather than
failing: an unreachable feed, a private feed the SDK has no credentials for, an SDK too old for --format json, or a solution that was never restored still gets the drift analysis, which is computable from the
project files alone. The banner says which of those happened, and offers to run the restore when that is the
answer. A feed waiting on a sign-in prompt a child process can never show is given a time limit and then
explained rather than left to hang.
The page draws its own package icons from the package id, so a solution's worth of rows costs no image
requests, and the whole view holds still under default-src 'none' with a nonce - no remote script, font or
stylesheet anywhere in it. The matrix keeps its package column pinned while the projects scroll, shows only
the columns the visible rows actually use, and defaults to Problems only, which is what keeps a
sixty-project solution legible.
Changing packages runs the .NET SDK, so it needs a trusted workspace. Reading feeds is the one place this
extension talks to the network, and only while the window is open.
Bookmarks
Add Bookmark sits on files and folders in Solutions, and on the editor's own context menu - which is the
only place a method exists to be marked.
In the editor it marks whatever encloses the cursor: the method, property or constructor, named with the type
it lives in. The symbol comes from whichever language extension is serving the file, so a C# method needs the
C# extension awake; with no answer the line itself is marked, which is never wrong.
Bookmarks are kept in workspace state, so they come back when the workspace does, and they follow a file that
is renamed or moved - including a file moved along with its folder.
TODO
Every TODO, FIXME and HACK comment in the workspace, grouped by file, with the tag and line beside each.
The sweep does not start until the section is opened, results stream in as files are read, and saving a file
re-reads only that file. On a 3,600-file, 17 MB workspace the whole sweep takes ~590 ms, of which the
directory walk is 67 ms.
A tag has to follow a comment opener (//, /*, *, <!--, #), so "TODO: ..." inside a string literal
is left alone.
Run Configurations
One startup project, the launch profiles it declares, and what you can do with them:
Startup Project Api
Configuration Debug
Launch Profiles 3
http
https active
IIS Express IISExpress
Actions
Build Debug
Rebuild Debug
Clean Debug
Publish...
Set as Startup...
Edit launchSettings.json
Run and Debug sit in the top right of the editor, the way Visual Studio's toolbar carries them, and on
the section's own title bar - a green triangle and a blue bug, in colour rather than the toolbar's grey. They
appear as soon as a startup project is known - instantly in a workspace that has chosen one before, since
that is remembered in workspace state and costs no disk read to restore.
The extension wakes on a workspace holding a solution or project file, so the buttons are there without the
Solution Explorer ever having been opened. Where no project has been chosen yet, the search for one waits until
the window has painted.
The startup project is remembered in workspace state. When the workspace holds a single .sln or .slnx,
one is chosen for you the first time it is needed - the first web project, else the first executable, else the
first project the solution lists. With several solutions nothing is guessed; pick one with
Set as Startup Project..., which is also on the context menu of any project under Solutions.
Profiles come from the project's Properties/launchSettings.json, in the order the file lists them. The
active profile is the one you clicked, or the first commandName: "Project" profile. Profiles the .NET CLI
cannot start - IISExpress, Docker - are shown with their command name and say so if you try. Where the
project has no launchSettings.json the action turns into Create launchSettings.json and writes a
minimal one.
Run starts dotnet run --project <project> --launch-profile <profile> as the terminal's own process, so
nothing is quoted through a shell and the app still reads stdin. Re-running replaces the terminal.
Debug builds the project, finds what the build produced under bin/<configuration>, and hands VS Code a
coreclr launch configuration carrying the profile's environment, arguments, working directory and
applicationUrl - so it needs the C# extension for the debugger itself. A console app gets the integrated
terminal, a web app the internal console and a browser opened on the URL it reports.
Reading this section costs the solution file the tree would have read anyway plus one launchSettings.json.
No project file is read to draw it, and nothing is spawned until Run or Debug is clicked.
Build, Clean, Rebuild and Publish
All four are on the context menu of any solution or project under Solutions, in the Run Configurations
section for the startup project, and in the command palette - where, with nothing to go on, they ask which
solution or project you mean.
They run as VS Code tasks with the $msCompile problem matcher attached, which is the point: every
MSBuild warning and error becomes a Problems entry you can click through to the line. The SDK is started as a
process, not typed into a shell, so a path with a space in it cannot come apart on the way.
| Command |
What runs |
| Build |
dotnet build <target> -c <configuration> |
| Clean |
dotnet clean <target> -c <configuration> |
| Rebuild |
Clean, then Build - Visual Studio's Rebuild, spelled out, the build only starting if the clean succeeded |
| Publish |
dotnet publish <target> with whatever the picker collected |
Configuration
One configuration is in force at a time, shown on the Configuration row of the Run Configurations section
and used by Build, Clean, Rebuild, Run and Debug alike. Click the row - or run
Select Build Configuration... - to change it, and the choice is remembered in workspace state. Until you
pick one, csharpSolutionExplorer.buildConfiguration decides.
The list offered is Debug, Release, and anything else the target names: <Configurations> in a project
file, SolutionConfigurationPlatforms in a .sln, <BuildType> in a .slnx. Nothing is evaluated - it is
one read of a file the tree has usually read already.
Publish
The first pick is the whole thing for most publishes: a configuration, or Publish Again with the options
that target last used. More Options... opens the rest, and each step only appears where the project
leaves it open:
| Step |
Offered |
| Configuration |
The same list the Configuration row uses |
| Target framework |
Only when the project multi-targets |
| Runtime identifier |
Portable, the project's own <RuntimeIdentifiers>, then the common RIDs |
| Deployment mode |
Only when a runtime was chosen: framework-dependent or self-contained |
| Output folder |
The default bin/<configuration>/<framework>/publish, or one you browse to |
The answers are remembered per target, and every step starts on what was chosen last time, so a repeat
publish is one Enter per step. When it finishes, the notification carries the output folder with Reveal in
File Explorer and Copy Path - for a project, where the SDK put it; a solution publishes each of its
projects into a folder of its own, so there it just says it is done.
csharpSolutionExplorer.buildArguments and csharpSolutionExplorer.publishArguments append anything else you need
(-v:n, -p:PublishSingleFile=true) to dotnet build and dotnet publish respectively.
Tree line colour
Tree Line Colour..., on the Solutions view menu. It offers the palette the extension's own icons are
drawn in - solution purple, project green, web blue, package violet, amber - or any hex colour you like,
written as #RGB, #RGBA, #RRGGBB or #RRGGBBAA. csharpSolutionExplorer.treeLineColor is the setting
behind it.
One thing to know before turning it on: VS Code draws every tree in the window from a single pair of colours,
tree.indentGuidesStroke and tree.inactiveIndentGuidesStroke, and an extension cannot scope either to its
own view. So the setting is mirrored into your workbench.colorCustomizations, and the File Explorer, Search
and Debug trees get the same coloured lines.
It merges rather than replaces, so your other colour customizations are untouched; clearing the setting takes
the two values back out; and a line colour you have since changed by hand is left exactly as you set it,
because at that point it is yours and not ours to revoke.
Development
npm install
npm run watch # esbuild in watch mode
Then press F5 to launch an Extension Development Host.
npm run typecheck # tsc --noEmit
npm run build # production bundle into dist/
npm run smoke # run the parsers against fixtures/ and print the parsed model
npm run nuget # check the NuGet side: versions, NuGet.Config, central versions, command lines
npm run nuget -- --live # the same, plus a round trip to nuget.org
npm run tree -- path/to/Some.sln # print a real solution the way the tree renders it
npm run endpoints # the endpoint reader and the explorer page
npm run diagrams # the four diagram pages and their filters
npm run lines # what the line colour writes to your settings
npm run icon # re-render media/icon.png from media/activity-bar.svg
npm run package # build a .vsix
fixtures/ holds a small workspace covering the interesting shapes: a classic .sln with nested solution
folders and solution items, the same solution as .slnx, a second solution in a subfolder, an SDK-style
project with package and project references, and a legacy non-SDK project with explicit compile items.
Icons
Everything the extension draws in the UI ships as SVG in media/ and needs no build step - the activity bar
logo included. The one exception is the marketplace logo, which the gallery wants as a raster:
scripts/gen-icons.ps1 copies the authored media/source/logo.png into media/icon.png and checks it
against what vsce will accept - at least 128x128, square, and a real PNG. media/source/ holds the
original design files and is not shipped (.vscodeignore), so the copy in media/ is the one that goes into
the package.
npm run icon
| Artwork |
Used for |
media/activity-bar.svg |
The activity bar logo that opens the view |
media/solution.svg |
Solution nodes |
media/solution-folder.svg |
Solution folder nodes |
media/project.svg |
Project nodes |
media/api-project.svg |
Projects built on Microsoft.NET.Sdk.Web |
media/folder.svg |
Folder nodes, overriding the file icon theme |
media/run.svg |
The Run button, in the editor's title bar and the Run Configurations title bar |
media/debug.svg |
The Debug button, in the same two places |
media/icon.png |
Marketplace logo - the only raster, installed from media/source/logo.png by npm run icon |
Run and Debug are SVG files rather than codicons on purpose: a codicon is a font glyph and always takes the
theme's icon foreground, so $(play) cannot be green. A file-based icon keeps its own colour, which is what
puts a green triangle and a blue bug in the title bar instead of two grey shapes.
Files keep whatever the active file icon theme gives them, so C# files still look like C# files.
Layout
src/
extension.ts activation, tree view, auto reveal
commands.ts command handlers
config.ts memoized settings
model/nodes.ts the node union rendered by the tree
parse/sln.ts classic .sln reader (single pass, line based)
parse/slnx.ts .slnx reader (tag scanner)
parse/project.ts .csproj reader + mtime-keyed cache
fs/dir.ts directory reads
fs/scan.ts breadth-first workspace scan that reports as it goes
tree/provider.ts TreeDataProvider: lazy children, caching, invalidation, reveal
tree/lines.ts the indent guide colour, mirrored into the workbench colours
util/csharp.ts reading C# as text: comments and string literals blanked in place
util/paths.ts MSBuild path resolution, case-aware keys, natural-order sorting
edit/clipboard.ts what was cut or copied, and the paste that follows
edit/fileOps.ts cut, copy, paste, delete and rename over workspace edits
edit/references.ts add and remove project references, and what each one brings
scaffold/target.ts the folder a new item lands in, and the names it accepts
scaffold/namespace.ts the namespace a folder implies, folded the way MSBuild folds it
scaffold/templates.ts the Class, Interface, Record, Struct and Enum bodies
scaffold/newItem.ts Add New Item...
scaffold/newEntry.ts Add Directory... and Add Scratch File...
scaffold/projectItems.ts the <Compile>/<None> item a legacy project needs
dotnet/cli.ts the SDK as a child process, and the output channel it writes to
dotnet/templates.ts the template list read from dotnet new list
dotnet/newProject.ts Add New Project..., and filing it in the solution folder
bookmarks/store.ts bookmarks in workspace state, following renames and moves
bookmarks/commands.ts marking a folder, a file, or the symbol under the cursor
bookmarks/provider.ts the Bookmarks section
todo/model.ts a tag, its line and the file it came from
todo/provider.ts the TODO sweep, streamed, and the re-read on save
diagram/graph.ts the project reference graph in scope, cycles included
diagram/packages.ts the bipartite project-to-package graph
diagram/layout.ts layering and median sweeps, the crossings taken out
diagram/html.ts page furniture shared by all four, so they read as one family
diagram/render.ts the reference graph and the reference matrix
diagram/renderPackages.ts the package graph and the package matrix
diagram/panels.ts the four tabs and the messages they answer
deadcode/analyze.ts declarations, mentions, base lists and names in string literals
deadcode/scan.ts the sweep over a solution, and what discounts a mention
deadcode/model.ts categories and the three confidences
deadcode/render.ts the report page, grouped and filtered
deadcode/panel.ts the tool window
nuget/model.ts package vocabulary + SemVer 2 comparison
nuget/sources.ts the NuGet.Config chain: which feeds, and which are enabled
nuget/client.ts NuGet V3 over node:https, cached and de-duplicated
nuget/scope.ts installed packages per project, incl. Directory.Packages.props
nuget/health.ts drift, vulnerabilities, deprecation via dotnet list package
nuget/assets.ts project.assets.json: what pulled a transitive package in
nuget/migrate.ts Central Package Management migration: plan, diff, apply, undo
nuget/ops.ts dotnet add/remove package in a reused terminal
nuget/panel.ts the tool window: feeds in, SDK out, watcher for the result
nuget/render.ts the page itself
run/target.ts the projects the run view can start, from the solution alone
run/model.ts launchSettings.json for the startup project, and nothing else
run/store.ts the startup project and its launch profile, in workspace state
run/provider.ts the Run Configurations section
run/commands.ts Run, Debug, startup project, launch profiles
build/model.ts build targets and the configurations they name
build/store.ts the active configuration, and what publish was last asked for per target
build/tasks.ts dotnet as a VS Code task, with the $msCompile matcher
build/publish.ts the publish picker, its arguments and its output folder
build/commands.ts Build, Clean, Rebuild, Publish
endpoints/model.ts verbs, parameters and the DTO shapes behind them
endpoints/parse.ts attribute-routed controllers and minimal APIs, read as text
endpoints/bind.ts C# signatures: which value goes in the path, query, header or body
endpoints/schema.ts payload examples and query models flattened into fields
endpoints/scan.ts the sweep over a solution, and the URLs in launchSettings.json
endpoints/http.ts the try-it request, made from the extension host
endpoints/panel.ts the tool window and the messages it answers
endpoints/render.ts the page itself
media/ icon artwork, SVG except the marketplace logo
scripts/
gen-icons.ps1 renders media/icon.png from media/activity-bar.svg
smoke.ts parser harness run by npm run smoke
nuget-probe.ts NuGet checks run by npm run nuget
sln-tree.ts prints a real .sln as the tree renders it (npm run tree)
endpoint-probe.ts endpoint reader and explorer page checks (npm run endpoints)
diagram-probe.ts runs the four diagram pages and works their filters (npm run diagrams)
lines-probe.ts what the line colour writes to your settings (npm run lines)
bench-view.ts tree open and expand timings against a real workspace
vscode-stub.ts enough of the vscode API to run the tree in a plain Node process
Roadmap
Search within a solution, and a Test Explorer over dotnet test.
License
MIT