mirror of
https://github.com/godotengine/godot-csharp-vscode.git
synced 2026-01-03 10:09:13 +03:00
Refactor extension and create asset generator
Separates the extension.ts code in multiple files and adds an asset generator code to provide the `tasks.json` and `launch.json` files (can be triggered manually with a command). - The build task generated uses the Godot CLI to build the solution (as described in #18), I think this is the cleanest solution since we let Godot handle the building of the project, but the user must configure the path to the Godot executable. - The Launch debug configuration is now created with the build `preLaunchTask` automatically so users won't have to set it up manually (fixes #18). - Adds snippets to `tasks.json` to add debug configuration easily after the file has been created. - Adds a command to generate the `tasks.json` and `launch.json` manually for convenience (asks the user if they want to override their current configuration first).
This commit is contained in:
88
package.json
88
package.json
@@ -19,7 +19,9 @@
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:**/project.godot",
|
||||
"onDebugResolve:godot"
|
||||
"onDebugResolve:godot",
|
||||
"onCommand:godot.csharp.selectProject",
|
||||
"onCommand:godot.csharp.generateAssets"
|
||||
],
|
||||
"main": "./dist/extension.bundled.js",
|
||||
"scripts": {
|
||||
@@ -33,7 +35,10 @@
|
||||
"webpack-watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-file": "^2.0.2",
|
||||
"chokidar": "^3.4.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"promise-socket": "^6.0.3",
|
||||
"vscode-debugprotocol": "^1.40.0"
|
||||
},
|
||||
@@ -42,10 +47,11 @@
|
||||
"ms-vscode.mono-debug"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/vscode": "^1.28.0",
|
||||
"@types/node": "^16.4.1",
|
||||
"@types/vscode": "^1.62.0",
|
||||
"glob": "^7.1.4",
|
||||
"make": "^0.8.1",
|
||||
"mocha": "^6.1.4",
|
||||
@@ -65,16 +71,29 @@
|
||||
}
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "godot.csharp.generateAssets",
|
||||
"title": "Generate Assets for Build and Debug",
|
||||
"category": "C# Godot"
|
||||
}
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "csharp"
|
||||
},
|
||||
{
|
||||
"language": "fsharp"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "godot-mono",
|
||||
"label": "C# Godot",
|
||||
"enableBreakpointsFor": {
|
||||
"languageIds": [
|
||||
"csharp",
|
||||
"fsharp"
|
||||
]
|
||||
},
|
||||
"languages": [
|
||||
"csharp",
|
||||
"fsharp"
|
||||
],
|
||||
"program": "./dist/GodotDebugSession/GodotDebugSession.exe",
|
||||
"osx": {
|
||||
"runtime": "mono"
|
||||
@@ -82,30 +101,43 @@
|
||||
"linux": {
|
||||
"runtime": "mono"
|
||||
},
|
||||
"initialConfigurations": [
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"name": "Play in Editor",
|
||||
"type": "godot-mono",
|
||||
"mode": "playInEditor",
|
||||
"request": "launch"
|
||||
"label": "C# Godot: Play in Editor Configuration",
|
||||
"description": "Launch a C# Godot App from the open editor with a debugger.",
|
||||
"body": {
|
||||
"name": "Play in Editor",
|
||||
"type": "godot-mono",
|
||||
"mode": "playInEditor",
|
||||
"request": "launch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch",
|
||||
"type": "godot-mono",
|
||||
"request": "launch",
|
||||
"mode": "executable",
|
||||
"executable": "${workspaceRoot}/Godot.exe",
|
||||
"executableArguments": [
|
||||
"--path",
|
||||
"${workspaceRoot}"
|
||||
]
|
||||
"label": "C# Godot: Launch Configuration",
|
||||
"description": "Launch a C# Godot App with a debugger.",
|
||||
"body": {
|
||||
"name": "Launch",
|
||||
"type": "godot-mono",
|
||||
"request": "launch",
|
||||
"mode": "executable",
|
||||
"preLaunchTask": "build",
|
||||
"executable": "${1:<insert-godot-executable-path-here>}",
|
||||
"executableArguments": [
|
||||
"--path",
|
||||
"${workspaceRoot}"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach",
|
||||
"type": "godot-mono",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 23685
|
||||
"label": "C# Godot: Attach Configuration",
|
||||
"description": "Attach a debugger to a C# Godot App.",
|
||||
"body": {
|
||||
"name": "Attach",
|
||||
"type": "godot-mono",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 23685
|
||||
}
|
||||
}
|
||||
],
|
||||
"configurationAttributes": {
|
||||
|
||||
42
src/assets-generator/assets-generator.ts
Normal file
42
src/assets-generator/assets-generator.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import {addTasksJsonIfNecessary} from './tasks';
|
||||
import {addLaunchJsonIfNecessary} from './debug';
|
||||
import {getVscodeFolder} from '../vscode-utils';
|
||||
|
||||
export class AssetsGenerator {
|
||||
public readonly vscodeFolder: string;
|
||||
public readonly tasksJsonPath: string;
|
||||
public readonly launchJsonPath: string;
|
||||
|
||||
private constructor(vscodeFolder: string)
|
||||
{
|
||||
this.vscodeFolder = vscodeFolder;
|
||||
this.tasksJsonPath = path.join(vscodeFolder, 'tasks.json');
|
||||
this.launchJsonPath = path.join(vscodeFolder, 'launch.json');
|
||||
}
|
||||
|
||||
public static Create(vscodeFolder: string): AssetsGenerator;
|
||||
public static Create(vscodeFolder: string | undefined = undefined): AssetsGenerator | undefined
|
||||
{
|
||||
vscodeFolder = vscodeFolder ?? getVscodeFolder();
|
||||
if (!vscodeFolder)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new AssetsGenerator(vscodeFolder);
|
||||
}
|
||||
|
||||
public async addTasksJsonIfNecessary(): Promise<void> {
|
||||
return addTasksJsonIfNecessary(this.tasksJsonPath);
|
||||
}
|
||||
|
||||
public async addLaunchJsonIfNecessary(): Promise<void> {
|
||||
return addLaunchJsonIfNecessary(this.launchJsonPath);
|
||||
}
|
||||
|
||||
public async hasExistingAssets(): Promise<boolean> {
|
||||
return (await fs.pathExists(this.tasksJsonPath)) || (await fs.pathExists(this.launchJsonPath));
|
||||
}
|
||||
}
|
||||
107
src/assets-generator/debug.ts
Normal file
107
src/assets-generator/debug.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import * as fs from 'fs-extra';
|
||||
import {getFormattingOptions, replaceCommentPropertiesWithComments, updateJsonWithComments} from '../json-utils';
|
||||
|
||||
export function createLaunchConfiguration():
|
||||
{version: string, configurations: vscode.DebugConfiguration[]}
|
||||
{
|
||||
return {
|
||||
version: '2.0.0',
|
||||
configurations: _createDebugConfigurations(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDebugConfigurationsArray(): vscode.DebugConfiguration[]
|
||||
{
|
||||
const configurations = _createDebugConfigurations();
|
||||
|
||||
// Remove comments
|
||||
configurations.forEach(configuration => {
|
||||
for (const key in configuration)
|
||||
{
|
||||
if (Object.prototype.hasOwnProperty.call(configuration, key))
|
||||
{
|
||||
if (key.startsWith('OS-COMMENT'))
|
||||
{
|
||||
delete configuration[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return configurations;
|
||||
}
|
||||
|
||||
function _createDebugConfigurations(): vscode.DebugConfiguration[]
|
||||
{
|
||||
return [
|
||||
createPlayInEditorDebugConfiguration(),
|
||||
createLaunchDebugConfiguration(),
|
||||
createAttachDebugConfiguration(),
|
||||
];
|
||||
}
|
||||
|
||||
export function createPlayInEditorDebugConfiguration(): vscode.DebugConfiguration
|
||||
{
|
||||
return {
|
||||
name: 'Play in Editor',
|
||||
type: 'godot-mono',
|
||||
mode: 'playInEditor',
|
||||
request: 'launch',
|
||||
};
|
||||
}
|
||||
|
||||
export function createLaunchDebugConfiguration(): vscode.DebugConfiguration
|
||||
{
|
||||
return {
|
||||
name: 'Launch',
|
||||
type: 'godot-mono',
|
||||
request: 'launch',
|
||||
mode: 'executable',
|
||||
preLaunchTask: 'build',
|
||||
executable: '<insert-godot-executable-path-here>',
|
||||
'OS-COMMENT1': 'See which arguments are available here:',
|
||||
'OS-COMMENT2': 'https://docs.godotengine.org/en/stable/getting_started/editor/command_line_tutorial.html',
|
||||
executableArguments: [
|
||||
'--path',
|
||||
'${workspaceRoot}',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function createAttachDebugConfiguration()
|
||||
{
|
||||
return {
|
||||
name: 'Attach',
|
||||
type: 'godot-mono',
|
||||
request: 'attach',
|
||||
address: 'localhost',
|
||||
port: 23685,
|
||||
};
|
||||
}
|
||||
|
||||
export async function addLaunchJsonIfNecessary(launchJsonPath: string): Promise<void>
|
||||
{
|
||||
const launchConfiguration = createLaunchConfiguration();
|
||||
|
||||
const formattingOptions = getFormattingOptions();
|
||||
|
||||
let text: string;
|
||||
const exists = await fs.pathExists(launchJsonPath);
|
||||
if (!exists) {
|
||||
// when launch.json does not exist, create it and write all the content directly
|
||||
const launchJsonText = JSON.stringify(launchConfiguration);
|
||||
const launchJsonTextFormatted = jsonc.applyEdits(launchJsonText, jsonc.format(launchJsonText, undefined, formattingOptions));
|
||||
text = launchJsonTextFormatted;
|
||||
} else {
|
||||
// when launch.json exists replace or append our configurations
|
||||
const ourConfigs = launchConfiguration.configurations ?? [];
|
||||
const content = fs.readFileSync(launchJsonPath).toString();
|
||||
const updatedJson = updateJsonWithComments(content, ourConfigs, 'configurations', 'name', formattingOptions);
|
||||
text = updatedJson;
|
||||
}
|
||||
|
||||
const textWithComments = replaceCommentPropertiesWithComments(text);
|
||||
await fs.writeFile(launchJsonPath, textWithComments);
|
||||
}
|
||||
3
src/assets-generator/index.ts
Normal file
3
src/assets-generator/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './tasks';
|
||||
export * from './debug';
|
||||
export * from './assets-generator';
|
||||
48
src/assets-generator/tasks.ts
Normal file
48
src/assets-generator/tasks.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as tasks from 'vscode-tasks';
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import * as fs from 'fs-extra';
|
||||
import {getFormattingOptions, replaceCommentPropertiesWithComments, updateJsonWithComments} from '../json-utils';
|
||||
|
||||
export function createTasksConfiguration(): tasks.TaskConfiguration
|
||||
{
|
||||
return {
|
||||
version: '2.0.0',
|
||||
tasks: [createBuildTaskDescription()],
|
||||
};
|
||||
}
|
||||
|
||||
export function createBuildTaskDescription(): tasks.TaskDescription
|
||||
{
|
||||
return {
|
||||
label: 'build',
|
||||
command: '<insert-godot-executable-path-here>',
|
||||
type: 'process',
|
||||
args: ['--build-solutions', '--path', '${workspaceRoot}', '--no-window', '-q'],
|
||||
problemMatcher: '$msCompile',
|
||||
};
|
||||
}
|
||||
|
||||
export async function addTasksJsonIfNecessary(tasksJsonPath: string): Promise<void>
|
||||
{
|
||||
const tasksConfiguration = createTasksConfiguration();
|
||||
|
||||
const formattingOptions = getFormattingOptions();
|
||||
|
||||
let text: string;
|
||||
const exists = await fs.pathExists(tasksJsonPath);
|
||||
if (!exists) {
|
||||
// when tasks.json does not exist create it and write all the content directly
|
||||
const tasksJsonText = JSON.stringify(tasksConfiguration);
|
||||
const tasksJsonTextFormatted = jsonc.applyEdits(tasksJsonText, jsonc.format(tasksJsonText, undefined, formattingOptions));
|
||||
text = tasksJsonTextFormatted;
|
||||
} else {
|
||||
// when tasks.json exists just update the tasks node
|
||||
const ourConfigs = tasksConfiguration.tasks ?? [];
|
||||
const content = fs.readFileSync(tasksJsonPath).toString();
|
||||
const updatedJson = updateJsonWithComments(content, ourConfigs, 'tasks', 'label', formattingOptions);
|
||||
text = updatedJson;
|
||||
}
|
||||
|
||||
const textWithComments = replaceCommentPropertiesWithComments(text);
|
||||
await fs.writeFile(tasksJsonPath, textWithComments);
|
||||
}
|
||||
51
src/assets-provider.ts
Normal file
51
src/assets-provider.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
import {getVscodeFolder} from './vscode-utils';
|
||||
import {AssetsGenerator} from './assets-generator';
|
||||
|
||||
export async function addAssets(): Promise<void>
|
||||
{
|
||||
const vscodeFolder = getVscodeFolder();
|
||||
if (!vscodeFolder)
|
||||
{
|
||||
vscode.window.showErrorMessage('Cannot generate C# Godot assets for build and debug. No workspace folder was selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const generator = AssetsGenerator.Create(vscodeFolder);
|
||||
|
||||
const doGenerateAssets = await shouldGenerateAssets(generator);
|
||||
if (!doGenerateAssets)
|
||||
{
|
||||
return; // user cancelled
|
||||
}
|
||||
|
||||
// Make sure .vscode folder exists, generator will fail to create tasks.json and launch.json if the folder does not exist.
|
||||
await fs.ensureDir(vscodeFolder);
|
||||
|
||||
const promises = [
|
||||
generator.addTasksJsonIfNecessary(),
|
||||
generator.addLaunchJsonIfNecessary(),
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async function shouldGenerateAssets(generator: AssetsGenerator): Promise<boolean>
|
||||
{
|
||||
if (await generator.hasExistingAssets()) {
|
||||
const yesItem = {title: 'Yes'};
|
||||
const cancelItem = {title: 'Cancel', isCloseAffordance: true};
|
||||
const selection = await vscode.window.showWarningMessage('Replace existing build and debug assets?', cancelItem, yesItem);
|
||||
if (selection === yesItem)
|
||||
{
|
||||
return true;
|
||||
} else {
|
||||
// The user clicked cancel
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// The assets don't exist, so we're good to go.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
44
src/configuration.ts
Normal file
44
src/configuration.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
|
||||
// Too lazy so we're re-using mono-debug extension settings for now...
|
||||
const configuration = vscode.workspace.getConfiguration('mono-debug');
|
||||
|
||||
type ExceptionConfigurations = { [exception: string]: DebugProtocol.ExceptionBreakMode; };
|
||||
|
||||
const DEFAULT_EXCEPTIONS: ExceptionConfigurations = {
|
||||
'System.Exception': 'never',
|
||||
'System.SystemException': 'never',
|
||||
'System.ArithmeticException': 'never',
|
||||
'System.ArrayTypeMismatchException': 'never',
|
||||
'System.DivideByZeroException': 'never',
|
||||
'System.IndexOutOfRangeException': 'never',
|
||||
'System.InvalidCastException': 'never',
|
||||
'System.NullReferenceException': 'never',
|
||||
'System.OutOfMemoryException': 'never',
|
||||
'System.OverflowException': 'never',
|
||||
'System.StackOverflowException': 'never',
|
||||
'System.TypeInitializationException': 'never',
|
||||
};
|
||||
|
||||
export function getModel(): ExceptionConfigurations {
|
||||
let model = DEFAULT_EXCEPTIONS;
|
||||
if (configuration) {
|
||||
const exceptionOptions = configuration.get('exceptionOptions');
|
||||
if (exceptionOptions) {
|
||||
model = <ExceptionConfigurations>exceptionOptions;
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
export function convertToExceptionOptions(model: ExceptionConfigurations): DebugProtocol.ExceptionOptions[] {
|
||||
const exceptionItems: DebugProtocol.ExceptionOptions[] = [];
|
||||
for (let exception in model) {
|
||||
exceptionItems.push({
|
||||
path: [{ names: [exception] }],
|
||||
breakMode: model[exception],
|
||||
});
|
||||
}
|
||||
return exceptionItems;
|
||||
}
|
||||
51
src/debug-provider.ts
Normal file
51
src/debug-provider.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as configuration from './configuration';
|
||||
import * as fs from 'fs-extra';
|
||||
import {getVscodeFolder} from './vscode-utils';
|
||||
import {AssetsGenerator, createDebugConfigurationsArray} from './assets-generator';
|
||||
|
||||
export class GodotMonoDebugConfigProvider implements vscode.DebugConfigurationProvider {
|
||||
private godotProjectPath: string;
|
||||
|
||||
constructor(godotProjectPath: string) {
|
||||
this.godotProjectPath = godotProjectPath;
|
||||
}
|
||||
|
||||
public async provideDebugConfigurations(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<vscode.DebugConfiguration[]>
|
||||
{
|
||||
const vscodeFolder = getVscodeFolder();
|
||||
if (!folder || !folder.uri || !vscodeFolder)
|
||||
{
|
||||
vscode.window.showErrorMessage('Cannot create C# Godot debug configurations. No workspace folder was selected.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const generator = AssetsGenerator.Create(vscodeFolder);
|
||||
|
||||
// Make sure .vscode folder exists, addTasksJsonIfNecessary will fail to create tasks.json if the folder does not exist.
|
||||
await fs.ensureDir(vscodeFolder);
|
||||
|
||||
// Add a tasks.json
|
||||
await generator.addTasksJsonIfNecessary();
|
||||
|
||||
return createDebugConfigurationsArray();
|
||||
}
|
||||
|
||||
public async resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
debugConfiguration: vscode.DebugConfiguration,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<vscode.DebugConfiguration | undefined>
|
||||
{
|
||||
if (!debugConfiguration.__exceptionOptions) {
|
||||
debugConfiguration.__exceptionOptions = configuration.convertToExceptionOptions(configuration.getModel());
|
||||
}
|
||||
|
||||
debugConfiguration['godotProjectDir'] = this.godotProjectPath;
|
||||
|
||||
return debugConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { Client, Peer, MessageContent, MessageStatus, ILogger, IMessageHandler } from './godot-tools-messaging/client';
|
||||
import * as completion_provider from './completion-provider';
|
||||
import * as debug_provider from './debug-provider';
|
||||
import * as assets_provider from './assets-provider';
|
||||
import { fixPathForGodot } from './godot-utils';
|
||||
import { findProjectFiles, ProjectLocation, promptForProject } from './project-select'
|
||||
import { findProjectFiles, ProjectLocation, promptForProject } from './project-select';
|
||||
|
||||
let client: Client;
|
||||
let codeCompletionProvider: vscode.Disposable;
|
||||
@@ -89,40 +90,46 @@ class MessageHandler implements IMessageHandler {
|
||||
}
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
let foundProjects: ProjectLocation[] = await findProjectFiles();
|
||||
const foundProjects: ProjectLocation[] = await findProjectFiles();
|
||||
// No project.godot files found. The extension doesn't need to do anything more.
|
||||
if (foundProjects.length == 0) {
|
||||
if (foundProjects.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the status bar / project selector and prompt for project if necessary
|
||||
const commandId = 'csharpGodot.selectProject';
|
||||
const commandId = 'godot.csharp.selectProject';
|
||||
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
|
||||
statusBarItem.command = commandId;
|
||||
statusBarItem.show();
|
||||
context.subscriptions.push(statusBarItem);
|
||||
context.subscriptions.push(vscode.commands.registerCommand(commandId, async () => {
|
||||
let project = await promptForProject(); // project.godot
|
||||
const project = await promptForProject(); // project.godot
|
||||
if (project !== undefined) {
|
||||
setupProject(project, context);
|
||||
}
|
||||
}));
|
||||
|
||||
// One project.godot files found. Use it.
|
||||
if (foundProjects.length == 1) {
|
||||
if (foundProjects.length === 1) {
|
||||
setupProject(foundProjects[0], context);
|
||||
}
|
||||
// Multiple project.godot files found. Prompt the user for which one they want to use.
|
||||
else {
|
||||
let project = await promptForProject();
|
||||
const project = await promptForProject();
|
||||
if (project !== undefined) {
|
||||
setupProject(project, context);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup generate assets command
|
||||
const generateAssetsCommand = vscode.commands.registerCommand('godot.csharp.generateAssets', async () => {
|
||||
await assets_provider.addAssets();
|
||||
});
|
||||
context.subscriptions.push(generateAssetsCommand);
|
||||
}
|
||||
|
||||
function setupProject(project: ProjectLocation, context: vscode.ExtensionContext) {
|
||||
let statusBarPath:string = project.relativeProjectPath === '.' ? './' : project.relativeProjectPath;
|
||||
const statusBarPath:string = project.relativeProjectPath === '.' ? './' : project.relativeProjectPath;
|
||||
statusBarItem.text = `$(folder) Godot Project: ${statusBarPath}`;
|
||||
// Setup client
|
||||
if (client !== undefined) {
|
||||
@@ -132,17 +139,17 @@ function setupProject(project: ProjectLocation, context: vscode.ExtensionContext
|
||||
'VisualStudioCode',
|
||||
fixPathForGodot(project.absoluteProjectPath),
|
||||
new MessageHandler(),
|
||||
new Logger()
|
||||
new Logger(),
|
||||
);
|
||||
client.start();
|
||||
|
||||
// Setup debug provider
|
||||
if (debugConfigProvider) {
|
||||
if (debugConfigProvider !== undefined) {
|
||||
debugConfigProvider.dispose();
|
||||
}
|
||||
debugConfigProvider = vscode.debug.registerDebugConfigurationProvider(
|
||||
'godot-mono',
|
||||
new GodotMonoDebugConfigProvider(project.absoluteProjectPath)
|
||||
new debug_provider.GodotMonoDebugConfigProvider(project.absoluteProjectPath)
|
||||
);
|
||||
context.subscriptions.push(debugConfigProvider);
|
||||
|
||||
@@ -165,64 +172,3 @@ export function deactivate() {
|
||||
client.dispose();
|
||||
}
|
||||
|
||||
class GodotMonoDebugConfigProvider implements vscode.DebugConfigurationProvider {
|
||||
godotProjectPath: string;
|
||||
|
||||
constructor(godotProjectPath: string) {
|
||||
this.godotProjectPath = godotProjectPath;
|
||||
}
|
||||
|
||||
public async resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
debugConfiguration: vscode.DebugConfiguration,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<vscode.DebugConfiguration | undefined> {
|
||||
if (!debugConfiguration.__exceptionOptions) {
|
||||
debugConfiguration.__exceptionOptions = convertToExceptionOptions(getModel());
|
||||
}
|
||||
debugConfiguration['godotProjectDir'] = this.godotProjectPath;
|
||||
return debugConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
// Too lazy so we're re-using mono-debug extension settings for now...
|
||||
const configuration = vscode.workspace.getConfiguration('mono-debug');
|
||||
|
||||
type ExceptionConfigurations = { [exception: string]: DebugProtocol.ExceptionBreakMode; };
|
||||
|
||||
const DEFAULT_EXCEPTIONS: ExceptionConfigurations = {
|
||||
'System.Exception': 'never',
|
||||
'System.SystemException': 'never',
|
||||
'System.ArithmeticException': 'never',
|
||||
'System.ArrayTypeMismatchException': 'never',
|
||||
'System.DivideByZeroException': 'never',
|
||||
'System.IndexOutOfRangeException': 'never',
|
||||
'System.InvalidCastException': 'never',
|
||||
'System.NullReferenceException': 'never',
|
||||
'System.OutOfMemoryException': 'never',
|
||||
'System.OverflowException': 'never',
|
||||
'System.StackOverflowException': 'never',
|
||||
'System.TypeInitializationException': 'never'
|
||||
};
|
||||
|
||||
function getModel(): ExceptionConfigurations {
|
||||
let model = DEFAULT_EXCEPTIONS;
|
||||
if (configuration) {
|
||||
const exceptionOptions = configuration.get('exceptionOptions');
|
||||
if (exceptionOptions) {
|
||||
model = <ExceptionConfigurations>exceptionOptions;
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
function convertToExceptionOptions(model: ExceptionConfigurations): DebugProtocol.ExceptionOptions[] {
|
||||
const exceptionItems: DebugProtocol.ExceptionOptions[] = [];
|
||||
for (let exception in model) {
|
||||
exceptionItems.push({
|
||||
path: [{ names: [exception] }],
|
||||
breakMode: model[exception]
|
||||
});
|
||||
}
|
||||
return exceptionItems;
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ export class Client implements Disposable {
|
||||
await socket.connect(this.metadata!.port, 'localhost');
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.logError('Failed to connect to Godot Ide Server', err);
|
||||
this.logger.logError('Failed to connect to Godot Ide Server', err as Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
64
src/json-utils.ts
Normal file
64
src/json-utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import { FormattingOptions, ModificationOptions } from 'jsonc-parser';
|
||||
|
||||
export function getFormattingOptions(): FormattingOptions {
|
||||
const editorConfig = vscode.workspace.getConfiguration('editor');
|
||||
|
||||
const tabSize = editorConfig.get<number>('tabSize') ?? 4;
|
||||
const insertSpaces = editorConfig.get<boolean>('insertSpaces') ?? true;
|
||||
|
||||
const filesConfig = vscode.workspace.getConfiguration('files');
|
||||
const eolSetting = filesConfig.get<string>('eol');
|
||||
const eol = !eolSetting || eolSetting === 'auto' ? os.EOL : '\n';
|
||||
|
||||
const formattingOptions: FormattingOptions = {
|
||||
insertSpaces: insertSpaces,
|
||||
tabSize: tabSize,
|
||||
eol: eol,
|
||||
};
|
||||
|
||||
return formattingOptions;
|
||||
}
|
||||
|
||||
export function replaceCommentPropertiesWithComments(text: string): string {
|
||||
// replacing dummy properties OS-COMMENT with the normal comment syntax
|
||||
const regex = /["']OS-COMMENT\d*["']\s*\:\s*["'](.*)["']\s*?,/gi;
|
||||
const withComments = text.replace(regex, '// $1');
|
||||
|
||||
return withComments;
|
||||
}
|
||||
|
||||
export function updateJsonWithComments(text: string, replacements: any[], nodeName: string, keyName: string, formattingOptions: FormattingOptions) : string {
|
||||
const modificationOptions : ModificationOptions = {
|
||||
formattingOptions
|
||||
};
|
||||
|
||||
// parse using jsonc because there are comments
|
||||
// only use this to determine what to change
|
||||
// we will modify it as text to keep existing comments
|
||||
const parsed = jsonc.parse(text);
|
||||
const items = parsed[nodeName];
|
||||
const itemKeys : string[] = items.map((i: { [x: string]: string; }) => i[keyName]);
|
||||
|
||||
let modified = text;
|
||||
// count how many items we inserted to ensure we are putting items at the end
|
||||
// in the same order as they are in the replacements array
|
||||
let insertCount = 0;
|
||||
replacements.map((replacement: { [x: string]: string; }) => {
|
||||
const index = itemKeys.indexOf(replacement[keyName]);
|
||||
|
||||
const found = index >= 0;
|
||||
const modificationIndex = found ? index : items.length + insertCount++;
|
||||
const edits = jsonc.modify(modified, [nodeName, modificationIndex], replacement, modificationOptions);
|
||||
const updated = jsonc.applyEdits(modified, edits);
|
||||
|
||||
// we need to carry out the changes one by one, because we are inserting into the json
|
||||
// and so we cannot just figure out all the edits from the original text, instead we need to apply
|
||||
// changes one by one
|
||||
modified = updated;
|
||||
});
|
||||
|
||||
return replaceCommentPropertiesWithComments(modified);
|
||||
}
|
||||
@@ -2,34 +2,34 @@ import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface ProjectLocation{
|
||||
relativeFilePath: string
|
||||
absoluteFilePath: string
|
||||
relativeProjectPath: string
|
||||
absoluteProjectPath: string
|
||||
relativeFilePath: string;
|
||||
absoluteFilePath: string;
|
||||
relativeProjectPath: string;
|
||||
absoluteProjectPath: string;
|
||||
}
|
||||
|
||||
export async function findProjectFiles(): Promise<ProjectLocation[]> {
|
||||
let projectFiles = await vscode.workspace.findFiles("**/project.godot");
|
||||
const projectFiles = await vscode.workspace.findFiles("**/project.godot");
|
||||
return projectFiles.map((x) => {
|
||||
return {
|
||||
relativeFilePath: vscode.workspace.asRelativePath(x),
|
||||
absoluteFilePath: x.path,
|
||||
relativeProjectPath: path.dirname(vscode.workspace.asRelativePath(x)),
|
||||
absoluteProjectPath: path.dirname(x.path)
|
||||
}
|
||||
absoluteProjectPath: path.dirname(x.path),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptForProject(): Promise<ProjectLocation | undefined> {
|
||||
let godotProjectFiles = await findProjectFiles();
|
||||
let selectionOptions = godotProjectFiles?.map((x) => {
|
||||
const godotProjectFiles = await findProjectFiles();
|
||||
const selectionOptions = godotProjectFiles?.map((x) => {
|
||||
return {
|
||||
label: x.relativeFilePath,
|
||||
...x
|
||||
}
|
||||
...x,
|
||||
};
|
||||
});
|
||||
return vscode.window.showQuickPick(selectionOptions, {
|
||||
title: 'Select a Godot project',
|
||||
placeHolder: 'Select a Godot project'
|
||||
placeHolder: 'Select a Godot project',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
12
src/vscode-utils.ts
Normal file
12
src/vscode-utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
export function getVscodeFolder(): string | undefined
|
||||
{
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return path.join(workspaceFolders[0].uri.fsPath, '.vscode');
|
||||
}
|
||||
353
typings/vscode-tasks.d.ts
vendored
Normal file
353
typings/vscode-tasks.d.ts
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Copied from http://code.visualstudio.com/docs/editor/tasks_appendix
|
||||
|
||||
declare module "vscode-tasks" {
|
||||
export interface TaskConfiguration extends BaseTaskConfiguration {
|
||||
|
||||
/**
|
||||
* The configuration's version number
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* Windows specific task configuration
|
||||
*/
|
||||
windows?: BaseTaskConfiguration;
|
||||
|
||||
/**
|
||||
* Mac specific task configuration
|
||||
*/
|
||||
osx?: BaseTaskConfiguration;
|
||||
|
||||
/**
|
||||
* Linux specific task configuration
|
||||
*/
|
||||
linux?: BaseTaskConfiguration;
|
||||
}
|
||||
|
||||
export interface BaseTaskConfiguration {
|
||||
|
||||
/**
|
||||
* The type of a custom task. Tasks of type "shell" are executed
|
||||
* inside a shell (e.g. bash, cmd, powershell, ...)
|
||||
*/
|
||||
type?: "shell" | "process";
|
||||
|
||||
/**
|
||||
* The command to be executed. Can be an external program or a shell
|
||||
* command.
|
||||
*/
|
||||
command?: string;
|
||||
|
||||
/**
|
||||
* Specifies whether a global command is a background task.
|
||||
*/
|
||||
isBackground?: boolean;
|
||||
|
||||
/**
|
||||
* The command options used when the command is executed. Can be omitted.
|
||||
*/
|
||||
options?: CommandOptions;
|
||||
|
||||
/**
|
||||
* The arguments passed to the command. Can be omitted.
|
||||
*/
|
||||
args?: string[];
|
||||
|
||||
/**
|
||||
* The presentation options.
|
||||
*/
|
||||
presentation?: PresentationOptions;
|
||||
|
||||
/**
|
||||
* The problem matcher to be used if a global command is executed (e.g. no tasks
|
||||
* are defined). A tasks.json file can either contain a global problemMatcher
|
||||
* property or a tasks property but not both.
|
||||
*/
|
||||
problemMatcher?: string | ProblemMatcher | (string | ProblemMatcher)[];
|
||||
|
||||
/**
|
||||
* The configuration of the available tasks. A tasks.json file can either
|
||||
* contain a global problemMatcher property or a tasks property but not both.
|
||||
*/
|
||||
tasks?: TaskDescription[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Options to be passed to the external program or shell
|
||||
*/
|
||||
export interface CommandOptions {
|
||||
|
||||
/**
|
||||
* The current working directory of the executed program or shell.
|
||||
* If omitted Ticino's current workspace root is used.
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* The environment of the executed program or shell. If omitted
|
||||
* the parent process' environment is used.
|
||||
*/
|
||||
env?: { [key: string]: string; };
|
||||
|
||||
/**
|
||||
* Configuration of the shell when task type is `shell`
|
||||
*/
|
||||
shell: {
|
||||
|
||||
/**
|
||||
* The shell to use.
|
||||
*/
|
||||
executable: string;
|
||||
|
||||
/**
|
||||
* The arguments to be passed to the shell executable to run in command mode
|
||||
* (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe).
|
||||
*/
|
||||
args?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The description of a task.
|
||||
*/
|
||||
export interface TaskDescription {
|
||||
|
||||
/**
|
||||
* The task's name
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The type of a custom task. Tasks of type "shell" are executed
|
||||
* inside a shell (e.g. bash, cmd, powershell, ...)
|
||||
*/
|
||||
type: "shell" | "process";
|
||||
|
||||
/**
|
||||
* The command to execute. If the type is "shell" it should be the full
|
||||
* command line including any additional arguments passed to the command.
|
||||
*/
|
||||
command: string;
|
||||
|
||||
/**
|
||||
* Whether the executed command is kept alive and runs in the background.
|
||||
*/
|
||||
isBackground?: boolean;
|
||||
|
||||
/**
|
||||
* Additional arguments passed to the command. Should be used if type
|
||||
* is "process".
|
||||
*/
|
||||
args?: string[];
|
||||
|
||||
/**
|
||||
* Tasks V1 isBuildCommand is used to detect if the tasks is in a build group.
|
||||
*/
|
||||
isBuildCommand?: boolean;
|
||||
|
||||
/**
|
||||
* Defines the group to which this tasks belongs
|
||||
*/
|
||||
group?: "build" | "string";
|
||||
|
||||
/**
|
||||
* The presentation options.
|
||||
*/
|
||||
presentation?: PresentationOptions;
|
||||
|
||||
/**
|
||||
* The problem matcher(s) to use to capture problems in the tasks
|
||||
* output.
|
||||
*/
|
||||
problemMatcher?: string | ProblemMatcher | (string | ProblemMatcher)[];
|
||||
}
|
||||
|
||||
export interface PresentationOptions {
|
||||
|
||||
/**
|
||||
* Controls whether the task output is reveal in the user interface.
|
||||
* Defaults to `always`.
|
||||
*/
|
||||
reveal?: "never" | "silent" | "always";
|
||||
|
||||
/**
|
||||
* Controls whether the command associated with the task is echoed
|
||||
* in the user interface.
|
||||
*/
|
||||
echo?: boolean;
|
||||
|
||||
/**
|
||||
* Controls whether the panel showing the task output is taking focus.
|
||||
*/
|
||||
focus?: boolean;
|
||||
|
||||
/**
|
||||
* Controls if the task panel is used for this task only (dedicated),
|
||||
* shared between tasks (shared) or if a new panel is created on
|
||||
* every task execution (new). Defaults to `shared`
|
||||
*/
|
||||
panel?: "shared" | "dedicated" | "new";
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of a problem matcher that detects problems
|
||||
* in build output.
|
||||
*/
|
||||
export interface ProblemMatcher {
|
||||
|
||||
/**
|
||||
* The name of a base problem matcher to use. If specified the
|
||||
* base problem matcher will be used as a template and properties
|
||||
* specified here will replace properties of the base problem
|
||||
* matcher
|
||||
*/
|
||||
base?: string;
|
||||
|
||||
/**
|
||||
* The owner of the produced VS Code problem. This is typically
|
||||
* the identifier of a VS Code language service if the problems are
|
||||
* to be merged with the one produced by the language service
|
||||
* or 'external'. Defaults to 'external' if omitted.
|
||||
*/
|
||||
owner?: string;
|
||||
|
||||
/**
|
||||
* The severity of the VS Code problem produced by this problem matcher.
|
||||
*
|
||||
* Valid values are:
|
||||
* "error": to produce errors.
|
||||
* "warning": to produce warnings.
|
||||
* "info": to produce infos.
|
||||
*
|
||||
* The value is used if a pattern doesn't specify a severity match group.
|
||||
* Defaults to "error" if omitted.
|
||||
*/
|
||||
severity?: string;
|
||||
|
||||
/**
|
||||
* Defines how filename reported in a problem pattern
|
||||
* should be read. Valid values are:
|
||||
* - "absolute": the filename is always treated absolute.
|
||||
* - "relative": the filename is always treated relative to
|
||||
* the current working directory. This is the default.
|
||||
* - ["relative", "path value"]: the filename is always
|
||||
* treated relative to the given path value.
|
||||
*/
|
||||
fileLocation?: string | string[];
|
||||
|
||||
/**
|
||||
* The name of a predefined problem pattern, the inline definition
|
||||
* of a problem pattern or an array of problem patterns to match
|
||||
* problems spread over multiple lines.
|
||||
*/
|
||||
pattern?: string | ProblemPattern | ProblemPattern[];
|
||||
|
||||
/**
|
||||
* Additional information used to detect when a background task (like a watching task in Gulp)
|
||||
* is active.
|
||||
*/
|
||||
background?: BackgroundMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* A description to track the start and end of a background task.
|
||||
*/
|
||||
export interface BackgroundMatcher {
|
||||
|
||||
/**
|
||||
* If set to true the watcher is in active mode when the task
|
||||
* starts. This is equals of issuing a line that matches the
|
||||
* beginPattern.
|
||||
*/
|
||||
activeOnStart?: boolean;
|
||||
|
||||
/**
|
||||
* If matched in the output the start of a background task is signaled.
|
||||
*/
|
||||
beginsPattern?: string;
|
||||
|
||||
/**
|
||||
* If matched in the output the end of a background task is signaled.
|
||||
*/
|
||||
endsPattern?: string;
|
||||
}
|
||||
|
||||
export interface ProblemPattern {
|
||||
|
||||
/**
|
||||
* The regular expression to find a problem in the console output of an
|
||||
* executed task.
|
||||
*/
|
||||
regexp: string;
|
||||
|
||||
/**
|
||||
* The match group index of the filename.
|
||||
*/
|
||||
file: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problems's location. Valid location
|
||||
* patterns are: (line), (line,column) and (startLine,startColumn,endLine,endColumn).
|
||||
* If omitted the line and column properties are used.
|
||||
*/
|
||||
location?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's line in the source file.
|
||||
* Can only be omitted if location is specified.
|
||||
*/
|
||||
line?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's column in the source file.
|
||||
*/
|
||||
column?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's end line in the source file.
|
||||
*
|
||||
* Defaults to undefined. No end line is captured.
|
||||
*/
|
||||
endLine?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's end column in the source file.
|
||||
*
|
||||
* Defaults to undefined. No end column is captured.
|
||||
*/
|
||||
endColumn?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's severity.
|
||||
*
|
||||
* Defaults to undefined. In this case the problem matcher's severity
|
||||
* is used.
|
||||
*/
|
||||
severity?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the problem's code.
|
||||
*
|
||||
* Defaults to undefined. No code is captured.
|
||||
*/
|
||||
code?: number;
|
||||
|
||||
/**
|
||||
* The match group index of the message. Defaults to 0.
|
||||
*/
|
||||
message: number;
|
||||
|
||||
/**
|
||||
* Specifies if the last pattern in a multi line problem matcher should
|
||||
* loop as long as it does match a line consequently. Only valid on the
|
||||
* last problem pattern in a multi line problem matcher.
|
||||
*/
|
||||
loop?: boolean;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user