diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c4fe4..b9c3c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the "vscode-solution-explorer" extension will be documented in this file. -## 0.x.x +## 0.8.7 Fixing duplicated items in the tree view: "Element with id is already registered" error @@ -10,6 +10,10 @@ Fixing issue with terminal after executing the update all packages in project co Adding new nuget package management inline in a .csproj file. +Adding readonly support for .slnx files. + +Adding .slnx grammar support for highlighting. + ## 0.8.6 Bugfix #296: Ctr+Enter hotkey should have a "when" condition to not mess with other hotkeys diff --git a/README.md b/README.md index 359fc7b..e287f20 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ You can execute the "Open solution" command from the command palette or from the ### Omnisharp integration -You can enable omnisharp integration and vscode-solution-explorer will open the same .sln file you open with Microsoft extension. +You can enable omnisharp integration and vscode-solution-explorer will open the same .sln or .slnx file you open with Microsoft extension. ![Activate Omnisharp integration in settings panel](https://github.com/fernandoescolar/vscode-solution-explorer/raw/main/images/omnisharp-integration.png) @@ -240,6 +240,10 @@ This extension adds syntax highlighting to `.sln` files. ![Solution syntax highlighting](https://github.com/fernandoescolar/vscode-solution-explorer/raw/main/images/vscode-solution-explorer-sln-syntax.png) +And also for `.slnx` files. + +![Solution XML syntax highlighting](https://github.com/fernandoescolar/vscode-solution-explorer/raw/main/images/vscode-solution-explorer-slnx-syntax.png) + ## Extension Settings You can configure the extension in the Visual Studio Code settings panel: diff --git a/images/vscode-solution-explorer-slnx-syntax.png b/images/vscode-solution-explorer-slnx-syntax.png new file mode 100644 index 0000000..2d3315f Binary files /dev/null and b/images/vscode-solution-explorer-slnx-syntax.png differ diff --git a/package.json b/package.json index 664561d..2cacebb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-solution-explorer", "displayName": "vscode-solution-explorer", "description": "Visual Studio .sln file explorer for Visual Studio Code", - "version": "0.8.6", + "version": "0.8.7", "license": "MIT", "publisher": "fernandoescolar", "icon": "images/icon.png", @@ -26,8 +26,8 @@ "Other" ], "activationEvents": [ - "onView:slnexpl", "workspaceContains:**/*.sln", + "workspaceContains:**/*.slnx", "*" ], "main": "./out/extension", @@ -349,6 +349,11 @@ "command": "solutionExplorer.openSelectedSolution", "when": "resourceExtname == .sln", "group": "navigation" + }, + { + "command": "solutionExplorer.openSelectedSolution", + "when": "resourceExtname == .slnx", + "group": "navigation" } ], "view/title": [ @@ -761,6 +766,16 @@ ".sln" ], "configuration": "./syntaxes/sln.language.json" + }, + { + "id": "slnx", + "aliases": [ + "Solution XML File", + "sln" + ], + "extensions": [ + ".slnx" + ] } ], "grammars": [ @@ -768,6 +783,11 @@ "language": "sln", "scopeName": "source.solution", "path": "./syntaxes/sln.grammar.json" + }, + { + "language": "slnx", + "scopeName": "source.solutionx", + "path": "./syntaxes/slnx.grammar.json" } ], "configuration": { diff --git a/src/core/Solutions/SlnLoader.ts b/src/core/Solutions/SlnLoader.ts index 200115f..69443f2 100644 --- a/src/core/Solutions/SlnLoader.ts +++ b/src/core/Solutions/SlnLoader.ts @@ -14,7 +14,7 @@ export class SlnLoader { solution.type = SolutionType.Sln; solution.fullPath = filepath; solution.folderPath = path.dirname(filepath); - solution.name = path.basename(filepath); + solution.name = path.basename(filepath, path.extname(filepath)); sln.projects.filter(x => !x.parentProjectGuid).forEach(project => { const i = this.createSolutionItem(sln, project); solution.addItem(i); diff --git a/src/core/Solutions/index.ts b/src/core/Solutions/index.ts index d6d4827..dff1295 100644 --- a/src/core/Solutions/index.ts +++ b/src/core/Solutions/index.ts @@ -1,3 +1,10 @@ -export * from "./model"; +export { + Solution, + SolutionType, + SolutionItem, + SolutionFolder, + SolutionProject, + SolutionProjectType, +} from "./model"; export * from "./SolutionFactory"; export * from "../../SolutionFinder"; \ No newline at end of file diff --git a/src/core/Solutions/model.ts b/src/core/Solutions/model.ts index 5cab3de..4eb4172 100644 --- a/src/core/Solutions/model.ts +++ b/src/core/Solutions/model.ts @@ -17,7 +17,7 @@ class SolutionObject { } } -class SolutionParentObject extends SolutionObject { +export class SolutionParentObject extends SolutionObject { protected items: SolutionItem[] = []; public addItem(item: SolutionItem): void { diff --git a/src/core/Solutions/slnx/Slnx.ts b/src/core/Solutions/slnx/Slnx.ts index bc11e17..5dd2b20 100644 --- a/src/core/Solutions/slnx/Slnx.ts +++ b/src/core/Solutions/slnx/Slnx.ts @@ -1,7 +1,7 @@ import * as path from "@extensions/path"; import * as fs from "@extensions/fs"; import * as xml from "@extensions/xml"; -import { Solution, SolutionFolder, SolutionItem, SolutionProject, SolutionProjectType, SolutionType } from "../model"; +import { Solution, SolutionFolder, SolutionItem, SolutionParentObject, SolutionProject, SolutionProjectType, SolutionType } from "../model"; import { v4 as uuidv4 } from "uuid"; export class SlnxSolution extends Solution { @@ -17,7 +17,7 @@ export class SlnxSolution extends Solution { this.document = await xml.parseToJson(content); this.fullPath = filepath; this.folderPath = path.dirname(filepath); - this.name = path.basename(filepath); + this.name = path.basename(filepath, path.extname(filepath)); this.refresh(); } @@ -27,39 +27,61 @@ export class SlnxSolution extends Solution { } this.items = []; - if (this.document.elements.length === 1) { + if (this.document.elements.length === 1 && this.document.elements[0].name === 'Solution') { + this.document.elements[0].elements ||= []; this.document.elements[0].elements.forEach((child: { name: string; }) => { if (child.name === 'Folder') { - this.addItem(this.createFolder(child)); + this.addFolder(child, this); } else if (child.name === 'Project') { - this.addItem(this.createProject(child)); + this.addProject(child, this); } }); } } - private createProject(child: any) : SolutionItem { + private addProject(child: any, parent: SolutionParentObject) : SolutionItem { const project = new SolutionProject(uuidv4()); const projectPath = child.attributes.Path.replace(/\\/g, path.sep).trim(); project.fullPath = path.join(this.folderPath, projectPath); project.name = path.basename(projectPath, path.extname(projectPath)); project.type = SolutionProjectType.default; + + parent.addItem(project); return project; } - private createFolder(child: any) { - const folder = new SolutionFolder(uuidv4()); - folder.name = child.attributes.Name.replace(/^[\/\\]+|[\/\\]+$/g, ''); - folder.fullPath = path.join(this.folderPath, folder.name); + private addFolder(child: any, parent: SolutionParentObject) { + const names = child.attributes.Name.replace(/^[\/\\]+|[\/\\]+$/g, '').split(/[\/\\]/); + for (let i = 0; i < names.length; i++) { + if (!names[i]) continue; - child.elements.forEach((child: { name: string; }) => { + const existing = parent.getFolders().find((f) => f.name === names[i]); + if (existing) { + parent = existing; + } else { + const folder = new SolutionFolder(uuidv4()); + folder.name = names[i]; + folder.fullPath = path.join(this.folderPath, folder.name); + parent.addItem(folder); + parent = folder; + } + } + + child.elements ||= []; + child.elements.forEach((child: {attributes: any; name: string; }) => { if (child.name === 'Project') { - folder.addItem(this.createProject(child)) + this.addProject(child, parent); } else if (child.name === 'Folder') { - folder.addItem(this.createFolder(child)); + this.addFolder(child, parent); + } else if (child.name === 'File') { + const filepath = child.attributes.Path.replace(/\\/g, path.sep).trim(); + if (parent instanceof SolutionFolder) { + const filename = path.basename(filepath); + parent.solutionFiles[filename] = path.join(this.folderPath, filepath); + } } }); - return folder; + return parent; } } \ No newline at end of file diff --git a/src/tree/items/SolutionTreeItem.ts b/src/tree/items/SolutionTreeItem.ts index d575c34..4f4ced4 100644 --- a/src/tree/items/SolutionTreeItem.ts +++ b/src/tree/items/SolutionTreeItem.ts @@ -1,5 +1,5 @@ import { ISubscription, EventTypes, IEvent, IFileEvent, FileEventType } from "@events"; -import { Solution, SolutionFactory } from "@core/Solutions"; +import { Solution, SolutionFactory, SolutionType } from "@core/Solutions"; import { TreeItem, TreeItemCollapsibleState, TreeItemFactory, TreeItemContext, ContextValues } from "@tree"; export class SolutionTreeItem extends TreeItem { @@ -9,6 +9,7 @@ export class SolutionTreeItem extends TreeItem { super(context, context.solution.name, TreeItemCollapsibleState.Expanded, ContextValues.solution, context.solution.fullPath); this.allowIconTheme = false; this.subscription = context.eventAggregator.subscribe(EventTypes.file, evt => this.onFileEvent(evt)); + this.description = context.solution.type === SolutionType.Sln ? '' : 'readonly'; } public refreshContextValue(): void { diff --git a/syntaxes/slnx.grammar.json b/syntaxes/slnx.grammar.json new file mode 100644 index 0000000..0d7db63 --- /dev/null +++ b/syntaxes/slnx.grammar.json @@ -0,0 +1,103 @@ +{ + "name": "Visual Studio XML Solution File", + "scopeName": "source.solutionx", + "fileTypes": [ + "slnx" + ], + "patterns": [ + { + "include": "#main" + } + ], + "repository": { + "main": { + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#tag" + } + ] + }, + "tag": { + "begin": "()", + "name": "meta.tag.slnx", + "patterns": [ + { + "include": "#tagStuff" + } + ] + }, + "tagStuff": { + "patterns": [ + { + "captures": { + "1": { + "name": "entity.other.attribute-name.slnx" + } + }, + "match": "(?:^|\\s+)([-\\w.]+)\\s*=" + }, + { + "include": "#doublequotedString" + } + ] + }, + "doublequotedString": { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.slnx" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.slnx" + } + }, + "name": "string.quoted.double.slnx", + "patterns": [ + { + "include": "#entity" + } + ] + }, + "entity": { + "captures": { + "1": { + "name": "punctuation.definition.constant.xml" + }, + "3": { + "name": "punctuation.definition.constant.xml" + } + }, + "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)", + "name": "constant.character.entity.xml" + }, + "comments": { + "patterns": [ + { + "begin": "", + "name": "comment.block.slnx" + } + ] + } + } +} \ No newline at end of file