├── icon.png ├── .github ├── FUNDING.yml ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE │ ├── feature---enhancement-request.md │ └── bug-report.md ├── .gitignore ├── images ├── debugging.png ├── setupProjectCommand.png ├── codeCompletionSignals.png ├── setupDebuggingCommand.png ├── setupDebuggingCreate.png ├── setupProjectStatusbar.png ├── codeCompletionNodePaths.png ├── setupDebuggingGodotPath.png ├── codeCompletionInputActions.png └── setupDebuggingSelectGodot.png ├── src ├── assets-generator │ ├── index.ts │ ├── assets-generator.ts │ ├── tasks.ts │ └── debug.ts ├── vscode-utils.ts ├── workspace-utils.ts ├── project-select.ts ├── godot-utils.ts ├── assets-provider.ts ├── debug-provider.ts ├── configuration.ts ├── json-utils.ts ├── completion-provider.ts ├── extension.ts └── godot-tools-messaging │ └── client.ts ├── .vscodeignore ├── tslint.json ├── .gitmodules ├── .vscode ├── launch.json └── settings.json ├── GodotDebugSession ├── MonoDebug.cs ├── GodotMessageHandler.cs ├── Program.cs ├── ActionTextWriter.cs ├── Properties │ └── AssemblyInfo.cs ├── Logger.cs ├── GodotDebuggerStartInfo.cs ├── GodotDebugSession.sln ├── GodotDebugSession.csproj ├── GodotDebugSession.cs ├── GodotDebuggerSession.cs └── .gitignore ├── tsconfig.json ├── Makefile ├── LICENSE ├── webpack.config.js ├── format.sh ├── CHANGELOG.md ├── package.json ├── README.md └── typings └── vscode-tasks.d.ts /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/icon.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: godotengine 2 | custom: https://godotengine.org/donate 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /images/debugging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/debugging.png -------------------------------------------------------------------------------- /images/setupProjectCommand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupProjectCommand.png -------------------------------------------------------------------------------- /src/assets-generator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tasks'; 2 | export * from './debug'; 3 | export * from './assets-generator'; 4 | -------------------------------------------------------------------------------- /images/codeCompletionSignals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/codeCompletionSignals.png -------------------------------------------------------------------------------- /images/setupDebuggingCommand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupDebuggingCommand.png -------------------------------------------------------------------------------- /images/setupDebuggingCreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupDebuggingCreate.png -------------------------------------------------------------------------------- /images/setupProjectStatusbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupProjectStatusbar.png -------------------------------------------------------------------------------- /images/codeCompletionNodePaths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/codeCompletionNodePaths.png -------------------------------------------------------------------------------- /images/setupDebuggingGodotPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupDebuggingGodotPath.png -------------------------------------------------------------------------------- /images/codeCompletionInputActions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/codeCompletionInputActions.png -------------------------------------------------------------------------------- /images/setupDebuggingSelectGodot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-csharp-vscode/HEAD/images/setupDebuggingSelectGodot.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .gitmodules 2 | .gitignore 3 | .vscode/** 4 | .vscode-test/** 5 | out/** 6 | src/** 7 | .gitignore 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/tslint.json 11 | **/*.map 12 | **/*.ts 13 | node_modules/** 14 | webpack.config.js 15 | thirdparty/** 16 | GodotDebugSession/** 17 | !dist/** 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } 16 | -------------------------------------------------------------------------------- /src/vscode-utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | export function getVscodeFolder(): string | undefined 5 | { 6 | const workspaceFolders = vscode.workspace.workspaceFolders; 7 | if (!workspaceFolders) { 8 | return undefined; 9 | } 10 | 11 | return path.join(workspaceFolders[0].uri.fsPath, '.vscode'); 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Lint extension 12 | run: | 13 | sudo apt-get update -qq 14 | sudo apt-get install -qq dos2unix recode 15 | bash ./format.sh 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature---enhancement-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature / Enhancement Request 3 | about: Adding new features or improving existing ones. 4 | title: '' 5 | labels: enhancement 6 | assignees: neikeq 7 | 8 | --- 9 | 10 | 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/debugger-libs"] 2 | path = thirdparty/debugger-libs 3 | url = https://github.com/mono/debugger-libs.git 4 | [submodule "thirdparty/nrefactory"] 5 | path = thirdparty/nrefactory 6 | url = https://github.com/icsharpcode/NRefactory.git 7 | [submodule "thirdparty/vscode-mono-debug"] 8 | path = thirdparty/vscode-mono-debug 9 | url = https://github.com/neikeq/vscode-mono-debug.git 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: webpack-debug" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/workspace-utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function getWorkspaceScenes(projectDirectory: string | undefined = undefined): Promise { 4 | const pattern: string = '**/*.tscn'; 5 | const include: vscode.GlobPattern = projectDirectory 6 | ? new vscode.RelativePattern(projectDirectory, pattern) 7 | : pattern; 8 | return vscode.workspace.findFiles(include) 9 | .then(uris => uris.map(uri => uri.fsPath)); 10 | } 11 | -------------------------------------------------------------------------------- /GodotDebugSession/MonoDebug.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VSCodeDebug 4 | { 5 | // Placeholder for VSCodeDebug.Program in vscode-mono-debug 6 | static class Program 7 | { 8 | public static void Log(bool predicate, string format, params object[] data) 9 | { 10 | if (predicate) 11 | Log(format, data); 12 | } 13 | 14 | public static void Log(string format, params object[] data) 15 | { 16 | Console.Error.WriteLine(format, data); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using GodotTools.IdeMessaging; 3 | using GodotTools.IdeMessaging.Requests; 4 | 5 | namespace GodotDebugSession 6 | { 7 | public class GodotMessageHandler : ClientMessageHandler 8 | { 9 | protected override Task HandleOpenFile(OpenFileRequest request) 10 | { 11 | return Task.FromResult(new OpenFileResponse {Status = MessageStatus.RequestNotSupported}); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false // set this to true to hide the "out" folder with the compiled JS files 4 | }, 5 | "search.exclude": { 6 | "out": true // set this to false to include "out" folder in search results 7 | }, 8 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 9 | "typescript.tsc.autoDetect": "off", 10 | "files.watcherExclude": { 11 | "**/.git/objects/**": true, 12 | "**/.git/subtree-cache/**": true, 13 | "**/node_modules/**": true, 14 | "**/.hg/store/**": true, 15 | "thirdparty/**": true, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test", 20 | "thirdparty/vscode-mono-debug", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /GodotDebugSession/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GodotDebugSession 4 | { 5 | static class Program 6 | { 7 | static void Main() 8 | { 9 | try 10 | { 11 | Logger.Log("GodotDebugSession: Starting debug session..."); 12 | 13 | new GodotDebugSession() 14 | .Start(Console.OpenStandardInput(), Console.OpenStandardOutput()) 15 | .Wait(); 16 | 17 | Logger.Log("GodotDebugSession: Debug session terminated."); 18 | } 19 | catch (Exception ex) 20 | { 21 | Logger.LogError(ex); 22 | } 23 | finally 24 | { 25 | Logger.Close(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GodotDebugSession/ActionTextWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace GodotDebugSession 6 | { 7 | public class ActionTextWriter : TextWriter 8 | { 9 | private readonly StringBuilder buffer = new StringBuilder(); 10 | 11 | private Action Writer { get; } 12 | 13 | public ActionTextWriter(Action writer) 14 | { 15 | Writer = writer; 16 | } 17 | 18 | public override Encoding Encoding => Encoding.UTF8; 19 | 20 | public override void Write(char value) 21 | { 22 | if (value == '\n') 23 | { 24 | Writer(buffer.ToString()); 25 | buffer.Clear(); 26 | return; 27 | } 28 | 29 | buffer.Append(value); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug with the extension. 4 | title: '' 5 | labels: bug 6 | assignees: neikeq 7 | 8 | --- 9 | 10 | 16 | 17 | **OS/device including version:** 18 | 19 | 20 | 21 | **Issue description:** 22 | 23 | 24 | 25 | **Screenshots of issue:** 26 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOLUTION_DIR="./GodotDebugSession/" 2 | 3 | GODOT_DEBUG_SESSION="./dist/GodotDebugSession/GodotDebugSession.exe" 4 | 5 | all: package 6 | @echo "vsix created" 7 | 8 | package: build 9 | ./node_modules/.bin/vsce package 10 | 11 | publish: build 12 | ./node_modules/.bin/vsce publish 13 | 14 | build: $(GODOT_DEBUG_SESSION) tsc 15 | @echo "build finished" 16 | 17 | build-debug: $(GODOT_DEBUG_SESSION)-debug tsc-debug 18 | @echo "build finished" 19 | 20 | tsc: 21 | ./node_modules/.bin/tsc -p ./ 22 | ./node_modules/.bin/webpack --mode production 23 | 24 | tsc-debug: 25 | ./node_modules/.bin/tsc -p ./ 26 | ./node_modules/.bin/webpack --mode development 27 | 28 | $(GODOT_DEBUG_SESSION): 29 | msbuild /p:Configuration=Release /restore $(SOLUTION_DIR)/GodotDebugSession.sln 30 | 31 | $(GODOT_DEBUG_SESSION)-debug: 32 | msbuild /p:Configuration=Debug /restore $(SOLUTION_DIR)/GodotDebugSession.sln 33 | 34 | clean: 35 | msbuild /t:Clean $(SOLUTION_DIR)/GodotDebugSession.sln 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Godot Engine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/project-select.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | export interface ProjectLocation { 5 | relativeFilePath: string; 6 | absoluteFilePath: string; 7 | relativeProjectPath: string; 8 | absoluteProjectPath: string; 9 | } 10 | 11 | export async function findProjectFiles(): Promise { 12 | const projectFiles = await vscode.workspace.findFiles("**/project.godot"); 13 | return projectFiles.map((x) => { 14 | return { 15 | relativeFilePath: vscode.workspace.asRelativePath(x), 16 | absoluteFilePath: x.fsPath, 17 | relativeProjectPath: path.dirname(vscode.workspace.asRelativePath(x)), 18 | absoluteProjectPath: path.dirname(x.fsPath), 19 | }; 20 | }); 21 | } 22 | 23 | export async function promptForProject(): Promise { 24 | const godotProjectFiles = await findProjectFiles(); 25 | const selectionOptions = godotProjectFiles?.map((x) => { 26 | return { 27 | label: x.relativeFilePath, 28 | ...x, 29 | }; 30 | }); 31 | return vscode.window.showQuickPick(selectionOptions, { 32 | title: 'Select a Godot project', 33 | placeHolder: 'Select a Godot project', 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.bundled.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; 42 | -------------------------------------------------------------------------------- /src/godot-utils.ts: -------------------------------------------------------------------------------- 1 | import {lookpath} from 'lookpath'; 2 | import {Configuration} from './configuration'; 3 | import {client} from './extension'; 4 | 5 | export function fixPathForGodot(path: string): string { 6 | if (process.platform === "win32") { 7 | // Godot expects the drive letter to be upper case 8 | if (path && path.charAt(1) === ':') { 9 | let drive = path[0]; 10 | let driveUpper = drive.toUpperCase(); 11 | if (driveUpper !== drive) { 12 | path = driveUpper + path.substr(1); 13 | } 14 | } 15 | } 16 | 17 | return path; 18 | } 19 | 20 | export async function findGodotExecutablePath(): Promise 21 | { 22 | let path: string | undefined; 23 | 24 | // If the user has set the path in the settings, use that value 25 | path = Configuration.Value.godotExecutablePath; 26 | if (path) { 27 | return path; 28 | } 29 | 30 | // If the extension is connected to a running Godot editor instance, use its path 31 | if (client !== undefined) { 32 | path = client.metadata?.editorExecutablePath; 33 | if (path) { 34 | return path; 35 | } 36 | } 37 | 38 | // Check if the godot command is in the path 39 | path = await lookpath('godot'); 40 | if (path) { 41 | return path; 42 | } 43 | 44 | // We couldn't find the Godot executable 45 | return undefined; 46 | } 47 | -------------------------------------------------------------------------------- /src/assets-generator/assets-generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import {addTasksJsonIfNecessary} from './tasks'; 4 | import {addLaunchJsonIfNecessary} from './debug'; 5 | import {getVscodeFolder} from '../vscode-utils'; 6 | 7 | export class AssetsGenerator { 8 | public readonly vscodeFolder: string; 9 | public readonly tasksJsonPath: string; 10 | public readonly launchJsonPath: string; 11 | 12 | private constructor(vscodeFolder: string) 13 | { 14 | this.vscodeFolder = vscodeFolder; 15 | this.tasksJsonPath = path.join(vscodeFolder, 'tasks.json'); 16 | this.launchJsonPath = path.join(vscodeFolder, 'launch.json'); 17 | } 18 | 19 | public static Create(vscodeFolder: string): AssetsGenerator; 20 | public static Create(vscodeFolder: string | undefined = undefined): AssetsGenerator | undefined 21 | { 22 | vscodeFolder = vscodeFolder ?? getVscodeFolder(); 23 | if (!vscodeFolder) 24 | { 25 | return undefined; 26 | } 27 | 28 | return new AssetsGenerator(vscodeFolder); 29 | } 30 | 31 | public async addTasksJsonIfNecessary(): Promise { 32 | return addTasksJsonIfNecessary(this.tasksJsonPath); 33 | } 34 | 35 | public async addLaunchJsonIfNecessary(): Promise { 36 | return addLaunchJsonIfNecessary(this.launchJsonPath); 37 | } 38 | 39 | public async hasExistingAssets(): Promise { 40 | return (await fs.pathExists(this.tasksJsonPath)) || (await fs.pathExists(this.launchJsonPath)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uo pipefail 4 | IFS=$'\n\t' 5 | 6 | # Loops through all text files tracked by Git. 7 | git grep -zIl '' | 8 | while IFS= read -rd '' f; do 9 | # Exclude csproj and hdr files. 10 | if [[ "$f" == *"csproj" ]]; then 11 | continue 12 | elif [[ "$f" == *"hdr" ]]; then 13 | continue 14 | fi 15 | # Ensures that files are UTF-8 formatted. 16 | recode UTF-8 "$f" 2> /dev/null 17 | # Ensures that files have LF line endings. 18 | dos2unix "$f" 2> /dev/null 19 | # Ensures that files do not contain a BOM. 20 | sed -i '1s/^\xEF\xBB\xBF//' "$f" 21 | # Ensures that files end with newline characters. 22 | tail -c1 < "$f" | read -r _ || echo >> "$f"; 23 | # Remove trailing space characters. 24 | sed -z -i 's/\x20\x0A/\x0A/g' "$f" 25 | done 26 | 27 | git diff > patch.patch 28 | FILESIZE="$(stat -c%s patch.patch)" 29 | MAXSIZE=5 30 | 31 | # If no patch has been generated all is OK, clean up, and exit. 32 | if (( FILESIZE < MAXSIZE )); then 33 | printf "Files in this commit comply with the formatting rules.\n" 34 | rm -f patch.patch 35 | exit 0 36 | fi 37 | 38 | # A patch has been created, notify the user, clean up, and exit. 39 | printf "\n*** The following differences were found between the code " 40 | printf "and the formatting rules:\n\n" 41 | cat patch.patch 42 | printf "\n*** Aborting, please fix your commit(s) with 'git commit --amend' or 'git rebase -i '\n" 43 | rm -f patch.patch 44 | exit 1 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.2.1 4 | 5 | - Fix debugging not working on Windows due to wrong path separators (PR: [#35](https://github.com/godotengine/godot-csharp-vscode/pull/35)) 6 | - Update README with documentation for 0.2.0 features (PR: [#33](https://github.com/godotengine/godot-csharp-vscode/pull/33)) 7 | 8 | ## 0.2.0 9 | 10 | - Add `executableArguments` launch option (PR: [#21](https://github.com/godotengine/godot-csharp-vscode/pull/21)) 11 | - Assets and configuration providers (PR: [#22](https://github.com/godotengine/godot-csharp-vscode/pull/22)) 12 | - Generate `tasks.json` and `preLaunchTask` for `launch.json` 13 | - Attempt to find Godot executable path for configuring `tasks.json` and `launch.json` 14 | - Add snippets for `launch.json` configurations 15 | - Fix launch and debug configurations not working due to `package.json` using a deprecated property 16 | - Add debug configuration to launch a specified scene (PR: [#24](https://github.com/godotengine/godot-csharp-vscode/pull/24)) 17 | - Add support for different Godot project locations in workspace (PR: [#28](https://github.com/godotengine/godot-csharp-vscode/pull/28)) 18 | - Bump minimum required version of VS Code to v1.62 19 | 20 | Many thanks to @raulsntos and @olestourko 21 | 22 | ## 0.1.3 23 | 24 | - Fix communication with the Godot editor not working on Windows 25 | 26 | ## 0.1.2 27 | 28 | - Fix missing dependencies 29 | 30 | ## 0.1.1 31 | 32 | - Add extension icon 33 | 34 | ## 0.1.0 35 | 36 | - Initial release 37 | -------------------------------------------------------------------------------- /GodotDebugSession/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("GodotDebugSession")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("GodotDebugSession")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("52059825-2921-4EFB-B6C3-350303F587EE")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /src/assets-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs-extra'; 3 | import {getVscodeFolder} from './vscode-utils'; 4 | import {AssetsGenerator} from './assets-generator'; 5 | 6 | export async function addAssets(): Promise 7 | { 8 | const vscodeFolder = getVscodeFolder(); 9 | if (!vscodeFolder) 10 | { 11 | vscode.window.showErrorMessage('Cannot generate C# Godot assets for build and debug. No workspace folder was selected.'); 12 | return; 13 | } 14 | 15 | const generator = AssetsGenerator.Create(vscodeFolder); 16 | 17 | const doGenerateAssets = await shouldGenerateAssets(generator); 18 | if (!doGenerateAssets) 19 | { 20 | return; // user cancelled 21 | } 22 | 23 | // Make sure .vscode folder exists, generator will fail to create tasks.json and launch.json if the folder does not exist. 24 | await fs.ensureDir(vscodeFolder); 25 | 26 | const promises = [ 27 | generator.addTasksJsonIfNecessary(), 28 | generator.addLaunchJsonIfNecessary(), 29 | ]; 30 | 31 | await Promise.all(promises); 32 | } 33 | 34 | async function shouldGenerateAssets(generator: AssetsGenerator): Promise 35 | { 36 | if (await generator.hasExistingAssets()) { 37 | const yesItem = {title: 'Yes'}; 38 | const cancelItem = {title: 'Cancel', isCloseAffordance: true}; 39 | const selection = await vscode.window.showWarningMessage('Replace existing build and debug assets?', cancelItem, yesItem); 40 | if (selection === yesItem) 41 | { 42 | return true; 43 | } else { 44 | // The user clicked cancel 45 | return false; 46 | } 47 | } else { 48 | // The assets don't exist, so we're good to go. 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /GodotDebugSession/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | 6 | namespace GodotDebugSession 7 | { 8 | static class Logger 9 | { 10 | private static string ThisAppPath => Assembly.GetExecutingAssembly().Location; 11 | private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null); 12 | 13 | private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log"; 14 | internal static readonly string NewLogPath = $"{ThisAppPathWithoutExtension}.new.log"; 15 | 16 | private static StreamWriter _writer; 17 | 18 | private static StreamWriter Writer => 19 | _writer ?? (_writer = new StreamWriter(LogPath, append: true, Encoding.UTF8)); 20 | 21 | private static void WriteLog(string message) 22 | { 23 | try 24 | { 25 | var writer = Writer; 26 | writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}"); 27 | writer.Flush(); 28 | } 29 | catch (IOException e) 30 | { 31 | Console.Error.WriteLine(e); 32 | } 33 | } 34 | 35 | public static void Log(string message) => 36 | WriteLog(message); 37 | 38 | public static void LogError(string message) => 39 | WriteLog(message); 40 | 41 | public static void LogError(string message, Exception ex) => 42 | WriteLog($"{message}\n{ex}"); 43 | 44 | public static void LogError(Exception ex) => 45 | WriteLog(ex.ToString()); 46 | 47 | public static void Close() 48 | { 49 | _writer?.Close(); 50 | _writer = null; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotDebuggerStartInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Mono.Debugging.Soft; 3 | 4 | namespace GodotDebugSession 5 | { 6 | public class GodotDebuggerStartInfo : SoftDebuggerStartInfo 7 | { 8 | public string GodotExecutablePath { get; } 9 | public string[] ExecutableArguments { get; } 10 | public ExecutionType ExecutionType { get; } 11 | public IProcessOutputListener ProcessOutputListener { get; } 12 | 13 | public GodotDebuggerStartInfo(ExecutionType executionType, string godotExecutablePath, 14 | string[] executableArguments, IProcessOutputListener processOutputListener, 15 | SoftDebuggerRemoteArgs softDebuggerConnectArgs) : 16 | base(softDebuggerConnectArgs) 17 | { 18 | ExecutionType = executionType; 19 | GodotExecutablePath = godotExecutablePath; 20 | ExecutableArguments = executableArguments; 21 | ProcessOutputListener = processOutputListener; 22 | } 23 | } 24 | 25 | public enum ExecutionType 26 | { 27 | PlayInEditor, 28 | Launch, 29 | Attach 30 | } 31 | 32 | public interface IProcessOutputListener 33 | { 34 | void ReceiveStdOut(string data); 35 | void ReceiveStdErr(string data); 36 | } 37 | 38 | public static class IProcessOutputListenerExtensions 39 | { 40 | public static TextWriter GetStdOutTextWriter(this IProcessOutputListener processOutputListener) 41 | { 42 | return new ActionTextWriter(processOutputListener.ReceiveStdOut); 43 | } 44 | 45 | public static TextWriter GetStdErrTextWriter(this IProcessOutputListener processOutputListener) 46 | { 47 | return new ActionTextWriter(processOutputListener.ReceiveStdErr); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/debug-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs-extra'; 3 | import {getVscodeFolder} from './vscode-utils'; 4 | import {Configuration} from './configuration'; 5 | import {AssetsGenerator, createDebugConfigurationsArray} from './assets-generator'; 6 | import {findGodotExecutablePath} from './godot-utils'; 7 | 8 | export class GodotMonoDebugConfigProvider implements vscode.DebugConfigurationProvider { 9 | private godotProjectPath: string; 10 | 11 | constructor(godotProjectPath: string) { 12 | this.godotProjectPath = godotProjectPath; 13 | } 14 | 15 | public async provideDebugConfigurations( 16 | folder: vscode.WorkspaceFolder | undefined, 17 | token?: vscode.CancellationToken 18 | ): Promise 19 | { 20 | const vscodeFolder = getVscodeFolder(); 21 | if (!folder || !folder.uri || !vscodeFolder) 22 | { 23 | vscode.window.showErrorMessage('Cannot create C# Godot debug configurations. No workspace folder was selected.'); 24 | return []; 25 | } 26 | 27 | const generator = AssetsGenerator.Create(vscodeFolder); 28 | 29 | // Make sure .vscode folder exists, addTasksJsonIfNecessary will fail to create tasks.json if the folder does not exist. 30 | await fs.ensureDir(vscodeFolder); 31 | 32 | // Add a tasks.json 33 | await generator.addTasksJsonIfNecessary(); 34 | 35 | const godotPath = await findGodotExecutablePath(); 36 | return createDebugConfigurationsArray(godotPath); 37 | } 38 | 39 | public async resolveDebugConfiguration( 40 | folder: vscode.WorkspaceFolder | undefined, 41 | debugConfiguration: vscode.DebugConfiguration, 42 | token?: vscode.CancellationToken 43 | ): Promise 44 | { 45 | if (!debugConfiguration.__exceptionOptions) { 46 | debugConfiguration.__exceptionOptions = Configuration.Value.exceptionOptionsForDebug; 47 | } 48 | 49 | debugConfiguration['godotProjectDir'] = this.godotProjectPath; 50 | 51 | return debugConfiguration; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/assets-generator/tasks.ts: -------------------------------------------------------------------------------- 1 | import * as tasks from 'vscode-tasks'; 2 | import * as jsonc from 'jsonc-parser'; 3 | import * as fs from 'fs-extra'; 4 | import {getFormattingOptions, replaceCommentPropertiesWithComments, updateJsonWithComments} from '../json-utils'; 5 | import {findGodotExecutablePath} from '../godot-utils'; 6 | 7 | export function createTasksConfiguration(godotExecutablePath: string | undefined): tasks.TaskConfiguration 8 | { 9 | return { 10 | version: '2.0.0', 11 | tasks: [createBuildTaskDescription(godotExecutablePath)], 12 | }; 13 | } 14 | 15 | export function createBuildTaskDescription(godotExecutablePath: string | undefined): tasks.TaskDescription 16 | { 17 | godotExecutablePath = godotExecutablePath ?? ''; 18 | return { 19 | label: 'build', 20 | command: godotExecutablePath, 21 | type: 'process', 22 | args: ['--build-solutions', '--path', '${workspaceRoot}', '--no-window', '-q'], 23 | problemMatcher: '$msCompile', 24 | }; 25 | } 26 | 27 | export async function addTasksJsonIfNecessary(tasksJsonPath: string): Promise 28 | { 29 | const godotExecutablePath = await findGodotExecutablePath(); 30 | const tasksConfiguration = createTasksConfiguration(godotExecutablePath); 31 | 32 | const formattingOptions = getFormattingOptions(); 33 | 34 | let text: string; 35 | const exists = await fs.pathExists(tasksJsonPath); 36 | if (!exists) { 37 | // when tasks.json does not exist create it and write all the content directly 38 | const tasksJsonText = JSON.stringify(tasksConfiguration); 39 | const tasksJsonTextFormatted = jsonc.applyEdits(tasksJsonText, jsonc.format(tasksJsonText, undefined, formattingOptions)); 40 | text = tasksJsonTextFormatted; 41 | } else { 42 | // when tasks.json exists just update the tasks node 43 | const ourConfigs = tasksConfiguration.tasks ?? []; 44 | const content = fs.readFileSync(tasksJsonPath).toString(); 45 | const updatedJson = updateJsonWithComments(content, ourConfigs, 'tasks', 'label', formattingOptions); 46 | text = updatedJson; 47 | } 48 | 49 | const textWithComments = replaceCommentPropertiesWithComments(text); 50 | await fs.writeFile(tasksJsonPath, textWithComments); 51 | } 52 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugProtocol } from 'vscode-debugprotocol'; 3 | 4 | type ExceptionConfigurations = { [exception: string]: DebugProtocol.ExceptionBreakMode; }; 5 | 6 | const DEFAULT_EXCEPTIONS: ExceptionConfigurations = { 7 | 'System.Exception': 'never', 8 | 'System.SystemException': 'never', 9 | 'System.ArithmeticException': 'never', 10 | 'System.ArrayTypeMismatchException': 'never', 11 | 'System.DivideByZeroException': 'never', 12 | 'System.IndexOutOfRangeException': 'never', 13 | 'System.InvalidCastException': 'never', 14 | 'System.NullReferenceException': 'never', 15 | 'System.OutOfMemoryException': 'never', 16 | 'System.OverflowException': 'never', 17 | 'System.StackOverflowException': 'never', 18 | 'System.TypeInitializationException': 'never', 19 | }; 20 | 21 | export class Configuration { 22 | public static Value: Configuration = new Configuration(); 23 | 24 | public godotExecutablePath: string | undefined; 25 | 26 | public exceptionOptions: ExceptionConfigurations = DEFAULT_EXCEPTIONS; 27 | 28 | public get exceptionOptionsForDebug(): DebugProtocol.ExceptionOptions[] { 29 | return this.convertToExceptionOptions(this.exceptionOptions); 30 | } 31 | 32 | private constructor(){ 33 | this.read(); 34 | vscode.workspace.onDidChangeConfiguration(e => { 35 | if (e.affectsConfiguration('godot.csharp') || e.affectsConfiguration('mono-debug')) 36 | { 37 | this.read(); 38 | } 39 | }); 40 | } 41 | 42 | public read() 43 | { 44 | const godotConfiguration = vscode.workspace.getConfiguration('godot.csharp'); 45 | // Too lazy so we're re-using mono-debug extension settings for now... 46 | const monoConfiguration = vscode.workspace.getConfiguration('mono-debug'); 47 | 48 | this.godotExecutablePath = godotConfiguration.get('executablePath'); 49 | this.exceptionOptions = monoConfiguration.get('exceptionOptions', DEFAULT_EXCEPTIONS); 50 | } 51 | 52 | private convertToExceptionOptions(model: ExceptionConfigurations): DebugProtocol.ExceptionOptions[] { 53 | const exceptionItems: DebugProtocol.ExceptionOptions[] = []; 54 | for (let exception in model) { 55 | exceptionItems.push({ 56 | path: [{ names: [exception] }], 57 | breakMode: model[exception], 58 | }); 59 | } 60 | return exceptionItems; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/json-utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as os from 'os'; 3 | import * as jsonc from 'jsonc-parser'; 4 | import { FormattingOptions, ModificationOptions } from 'jsonc-parser'; 5 | 6 | export function getFormattingOptions(): FormattingOptions { 7 | const editorConfig = vscode.workspace.getConfiguration('editor'); 8 | 9 | const tabSize = editorConfig.get('tabSize') ?? 4; 10 | const insertSpaces = editorConfig.get('insertSpaces') ?? true; 11 | 12 | const filesConfig = vscode.workspace.getConfiguration('files'); 13 | const eolSetting = filesConfig.get('eol'); 14 | const eol = !eolSetting || eolSetting === 'auto' ? os.EOL : '\n'; 15 | 16 | const formattingOptions: FormattingOptions = { 17 | insertSpaces: insertSpaces, 18 | tabSize: tabSize, 19 | eol: eol, 20 | }; 21 | 22 | return formattingOptions; 23 | } 24 | 25 | export function replaceCommentPropertiesWithComments(text: string): string { 26 | // replacing dummy properties OS-COMMENT with the normal comment syntax 27 | const regex = /["']OS-COMMENT\d*["']\s*\:\s*["'](.*)["']\s*?,/gi; 28 | const withComments = text.replace(regex, '// $1'); 29 | 30 | return withComments; 31 | } 32 | 33 | export function updateJsonWithComments(text: string, replacements: any[], nodeName: string, keyName: string, formattingOptions: FormattingOptions) : string { 34 | const modificationOptions : ModificationOptions = { 35 | formattingOptions 36 | }; 37 | 38 | // parse using jsonc because there are comments 39 | // only use this to determine what to change 40 | // we will modify it as text to keep existing comments 41 | const parsed = jsonc.parse(text); 42 | const items = parsed[nodeName]; 43 | const itemKeys : string[] = items.map((i: { [x: string]: string; }) => i[keyName]); 44 | 45 | let modified = text; 46 | // count how many items we inserted to ensure we are putting items at the end 47 | // in the same order as they are in the replacements array 48 | let insertCount = 0; 49 | replacements.map((replacement: { [x: string]: string; }) => { 50 | const index = itemKeys.indexOf(replacement[keyName]); 51 | 52 | const found = index >= 0; 53 | const modificationIndex = found ? index : items.length + insertCount++; 54 | const edits = jsonc.modify(modified, [nodeName, modificationIndex], replacement, modificationOptions); 55 | const updated = jsonc.applyEdits(modified, edits); 56 | 57 | // we need to carry out the changes one by one, because we are inserting into the json 58 | // and so we cannot just figure out all the edits from the original text, instead we need to apply 59 | // changes one by one 60 | modified = updated; 61 | }); 62 | 63 | return replaceCommentPropertiesWithComments(modified); 64 | } 65 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotDebugSession.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotDebugSession", "GodotDebugSession.csproj", "{52059825-2921-4EFB-B6C3-350303F587EE}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Debugging.Soft", "..\thirdparty\debugger-libs\Mono.Debugging.Soft\Mono.Debugging.Soft.csproj", "{DE40756E-57F6-4AF2-B155-55E3A88CCED8}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Debugging", "..\thirdparty\debugger-libs\Mono.Debugging\Mono.Debugging.csproj", "{90C99ADB-7D4B-4EB4-98C2-40BD1B14C7D2}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Debugger.Soft", "..\thirdparty\debugger-libs\Mono.Debugger.Soft\Mono.Debugger.Soft.csproj", "{372E8E3E-29D5-4B4D-88A2-4711CD628C4E}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ICSharpCode.NRefactory", "..\thirdparty\nrefactory\ICSharpCode.NRefactory\ICSharpCode.NRefactory.csproj", "{3B2A5653-EC97-4001-BB9B-D90F1AF2C371}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ICSharpCode.NRefactory.CSharp", "..\thirdparty\nrefactory\ICSharpCode.NRefactory.CSharp\ICSharpCode.NRefactory.CSharp.csproj", "{53DCA265-3C3C-42F9-B647-F72BA678122B}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {52059825-2921-4EFB-B6C3-350303F587EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {52059825-2921-4EFB-B6C3-350303F587EE}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {52059825-2921-4EFB-B6C3-350303F587EE}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {52059825-2921-4EFB-B6C3-350303F587EE}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {DE40756E-57F6-4AF2-B155-55E3A88CCED8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {DE40756E-57F6-4AF2-B155-55E3A88CCED8}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {DE40756E-57F6-4AF2-B155-55E3A88CCED8}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {DE40756E-57F6-4AF2-B155-55E3A88CCED8}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {90C99ADB-7D4B-4EB4-98C2-40BD1B14C7D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {90C99ADB-7D4B-4EB4-98C2-40BD1B14C7D2}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {90C99ADB-7D4B-4EB4-98C2-40BD1B14C7D2}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {90C99ADB-7D4B-4EB4-98C2-40BD1B14C7D2}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {372E8E3E-29D5-4B4D-88A2-4711CD628C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {372E8E3E-29D5-4B4D-88A2-4711CD628C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {372E8E3E-29D5-4B4D-88A2-4711CD628C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {372E8E3E-29D5-4B4D-88A2-4711CD628C4E}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {3B2A5653-EC97-4001-BB9B-D90F1AF2C371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {3B2A5653-EC97-4001-BB9B-D90F1AF2C371}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {3B2A5653-EC97-4001-BB9B-D90F1AF2C371}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {3B2A5653-EC97-4001-BB9B-D90F1AF2C371}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {53DCA265-3C3C-42F9-B647-F72BA678122B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {53DCA265-3C3C-42F9-B647-F72BA678122B}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {53DCA265-3C3C-42F9-B647-F72BA678122B}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {53DCA265-3C3C-42F9-B647-F72BA678122B}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /src/assets-generator/debug.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as jsonc from 'jsonc-parser'; 3 | import * as fs from 'fs-extra'; 4 | import {getFormattingOptions, replaceCommentPropertiesWithComments, updateJsonWithComments} from '../json-utils'; 5 | import {findGodotExecutablePath} from '../godot-utils'; 6 | 7 | export function createLaunchConfiguration(godotExecutablePath: string | undefined): 8 | {version: string, configurations: vscode.DebugConfiguration[]} 9 | { 10 | return { 11 | version: '2.0.0', 12 | configurations: _createDebugConfigurations(godotExecutablePath), 13 | }; 14 | } 15 | 16 | export function createDebugConfigurationsArray(godotExecutablePath: string | undefined): vscode.DebugConfiguration[] 17 | { 18 | const configurations = _createDebugConfigurations(godotExecutablePath); 19 | 20 | // Remove comments 21 | configurations.forEach(configuration => { 22 | for (const key in configuration) 23 | { 24 | if (Object.prototype.hasOwnProperty.call(configuration, key)) 25 | { 26 | if (key.startsWith('OS-COMMENT')) 27 | { 28 | delete configuration[key]; 29 | } 30 | } 31 | } 32 | }); 33 | 34 | return configurations; 35 | } 36 | 37 | function _createDebugConfigurations(godotExecutablePath: string | undefined): vscode.DebugConfiguration[] 38 | { 39 | return [ 40 | createPlayInEditorDebugConfiguration(), 41 | createLaunchDebugConfiguration(godotExecutablePath), 42 | createLaunchDebugConfiguration(godotExecutablePath, true), 43 | createAttachDebugConfiguration(), 44 | ]; 45 | } 46 | 47 | export function createPlayInEditorDebugConfiguration(): vscode.DebugConfiguration 48 | { 49 | return { 50 | name: 'Play in Editor', 51 | type: 'godot-mono', 52 | mode: 'playInEditor', 53 | request: 'launch', 54 | }; 55 | } 56 | 57 | export function createLaunchDebugConfiguration(godotExecutablePath: string | undefined, canSelectScene: boolean = false): vscode.DebugConfiguration 58 | { 59 | godotExecutablePath = godotExecutablePath ?? ''; 60 | return { 61 | name: `Launch${canSelectScene ? ' (Select Scene)' : ''}`, 62 | type: 'godot-mono', 63 | request: 'launch', 64 | mode: 'executable', 65 | preLaunchTask: 'build', 66 | executable: godotExecutablePath, 67 | 'OS-COMMENT1': 'See which arguments are available here:', 68 | 'OS-COMMENT2': 'https://docs.godotengine.org/en/stable/getting_started/editor/command_line_tutorial.html', 69 | executableArguments: [ 70 | '--path', 71 | '${workspaceRoot}', 72 | ...(canSelectScene ? ['${command:SelectLaunchScene}'] : []), 73 | ], 74 | }; 75 | } 76 | 77 | export function createAttachDebugConfiguration() 78 | { 79 | return { 80 | name: 'Attach', 81 | type: 'godot-mono', 82 | request: 'attach', 83 | address: 'localhost', 84 | port: 23685, 85 | }; 86 | } 87 | 88 | export async function addLaunchJsonIfNecessary(launchJsonPath: string): Promise 89 | { 90 | const godotExecutablePath = await findGodotExecutablePath(); 91 | const launchConfiguration = createLaunchConfiguration(godotExecutablePath); 92 | 93 | const formattingOptions = getFormattingOptions(); 94 | 95 | let text: string; 96 | const exists = await fs.pathExists(launchJsonPath); 97 | if (!exists) { 98 | // when launch.json does not exist, create it and write all the content directly 99 | const launchJsonText = JSON.stringify(launchConfiguration); 100 | const launchJsonTextFormatted = jsonc.applyEdits(launchJsonText, jsonc.format(launchJsonText, undefined, formattingOptions)); 101 | text = launchJsonTextFormatted; 102 | } else { 103 | // when launch.json exists replace or append our configurations 104 | const ourConfigs = launchConfiguration.configurations ?? []; 105 | const content = fs.readFileSync(launchJsonPath).toString(); 106 | const updatedJson = updateJsonWithComments(content, ourConfigs, 'configurations', 'name', formattingOptions); 107 | text = updatedJson; 108 | } 109 | 110 | const textWithComments = replaceCommentPropertiesWithComments(text); 111 | await fs.writeFile(launchJsonPath, textWithComments); 112 | } 113 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotDebugSession.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {52059825-2921-4EFB-B6C3-350303F587EE} 8 | Exe 9 | Properties 10 | GodotDebugSession 11 | GodotDebugSession 12 | v4.7.2 13 | 512 14 | ..\dist\GodotDebugSession\ 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | DebugSession.cs 43 | 44 | 45 | Handles.cs 46 | 47 | 48 | MonoDebugSession.cs 49 | 50 | 51 | Protocol.cs 52 | 53 | 54 | Utilities.cs 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {372e8e3e-29d5-4b4d-88a2-4711cd628c4e} 69 | Mono.Debugger.Soft 70 | 71 | 72 | {de40756e-57f6-4af2-b155-55e3a88cced8} 73 | Mono.Debugging.Soft 74 | 75 | 76 | {90c99adb-7d4b-4eb4-98c2-40bd1b14c7d2} 77 | Mono.Debugging 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotDebugSession.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using Mono.Debugging.Client; 4 | using Mono.Debugging.Soft; 5 | using VSCodeDebug; 6 | 7 | namespace GodotDebugSession 8 | { 9 | public class GodotDebugSession : MonoDebugSession, IProcessOutputListener 10 | { 11 | public GodotDebugSession() : base(new GodotDebuggerSession(), new CustomLogger()) 12 | { 13 | } 14 | 15 | public override void Launch(Response response, dynamic args) 16 | { 17 | ExecutionType executionType; 18 | 19 | string godotExecutablePath = getString(args, "executable"); 20 | 21 | string mode = getString(args, "mode"); 22 | if (string.IsNullOrEmpty(mode) || (mode != "playInEditor" && mode != "executable")) 23 | { 24 | executionType = !string.IsNullOrEmpty(godotExecutablePath) ? 25 | ExecutionType.Launch : 26 | ExecutionType.PlayInEditor; 27 | } 28 | else 29 | { 30 | executionType = mode == "playInEditor" ? ExecutionType.PlayInEditor : ExecutionType.Launch; 31 | } 32 | 33 | RunImpl(response, args, executionType); 34 | } 35 | 36 | public override void Attach(Response response, dynamic args) 37 | { 38 | RunImpl(response, args, ExecutionType.Attach); 39 | } 40 | 41 | private void RunImpl(Response response, dynamic args, ExecutionType executionType) 42 | { 43 | lock (_lock) 44 | { 45 | _attachMode = executionType == ExecutionType.Attach; 46 | 47 | SetExceptionBreakpoints(args.__exceptionOptions); 48 | 49 | SoftDebuggerRemoteArgs listenArgs; 50 | 51 | if (executionType == ExecutionType.Attach) 52 | { 53 | // validate argument 'address' 54 | string host = getString(args, "address"); 55 | if (host == null) 56 | { 57 | SendErrorResponse(response, 3007, "Property 'address' is missing or empty."); 58 | return; 59 | } 60 | 61 | // validate argument 'port' 62 | int port = getInt(args, "port", -1); 63 | if (port == -1) 64 | { 65 | SendErrorResponse(response, 3008, "Property 'port' is missing."); 66 | return; 67 | } 68 | 69 | IPAddress address = Utilities.ResolveIPAddress(host); 70 | if (address == null) 71 | { 72 | SendErrorResponse(response, 3013, "Invalid address '{host}'.", new { host }); 73 | return; 74 | } 75 | 76 | listenArgs = new SoftDebuggerConnectArgs("Godot", IPAddress.Loopback, port); 77 | } 78 | else 79 | { 80 | listenArgs = new SoftDebuggerListenArgs("Godot", IPAddress.Loopback, 0); 81 | } 82 | 83 | // ------ 84 | 85 | _debuggeeKilled = false; 86 | 87 | string godotExecutablePath = (string)args.executable; 88 | string[] executableArguments = args.executableArguments?.ToObject() ?? Array.Empty(); 89 | 90 | string godotProjectDir = (string)args.godotProjectDir; 91 | 92 | var startInfo = new GodotDebuggerStartInfo(executionType, 93 | godotExecutablePath, executableArguments, processOutputListener: this, listenArgs) 94 | { WorkingDirectory = godotProjectDir }; 95 | 96 | _session.Run(startInfo, _debuggerSessionOptions); 97 | 98 | _debuggeeExecuting = true; 99 | } 100 | } 101 | 102 | private class CustomLogger : ICustomLogger 103 | { 104 | public void LogError(string message, Exception ex) => Logger.LogError(message, ex); 105 | 106 | public void LogAndShowException(string message, Exception ex) => LogError(message, ex); 107 | 108 | public void LogMessage(string messageFormat, params object[] args) 109 | { 110 | Logger.Log(string.Format(messageFormat, args)); 111 | } 112 | 113 | public string GetNewDebuggerLogFilename() => Logger.NewLogPath; 114 | } 115 | 116 | public void ReceiveStdOut(string data) 117 | { 118 | if (data == null) 119 | _stdoutEOF = true; 120 | 121 | SendOutput("stdout", data); 122 | } 123 | 124 | public void ReceiveStdErr(string data) 125 | { 126 | if (data == null) 127 | _stdoutEOF = true; 128 | 129 | SendOutput("stderr", data); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/completion-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Client, MessageStatus } from './godot-tools-messaging/client'; 3 | import { fixPathForGodot } from './godot-utils'; 4 | 5 | enum CompletionKind { 6 | InputActions = 0, 7 | NodePaths, 8 | ResourcePaths, 9 | ScenePaths, 10 | ShaderParams, 11 | Signals, 12 | ThemeColors, 13 | ThemeConstants, 14 | ThemeFonts, 15 | ThemeStyles 16 | } 17 | 18 | const qualifiedNameRegex = /(?:((?!\d)\w+(?:[\n\r\s]*\.[\n\r\s]*(?!\d)\w+)*)[\n\r\s]*\.[\n\r\s]*)?((?!\d)\w+)/; 19 | const genericPartRegex = new RegExp('(?:[\\n\\r\\s]*<[\\n\\r\\s]*' + qualifiedNameRegex.source + '[\\n\\r\\s]*>)?'); 20 | 21 | const getNodeRegex = new RegExp(/\bGetNode/.source + genericPartRegex.source + /[\n\r\s]*\([\n\r\s]*(?"(?\w*))?$/.source); 22 | const inputActionRegex = /\b(IsActionPressed|IsActionJustPressed|IsActionJustReleased|GetActionStrength|ActionPress|ActionRelease)[\n\r\s]*\([\n\r\s]*(?"(?\w*))?$/; 23 | const resourcePathRegex = new RegExp(/\b(GD[\n\r\s]*\.[\n\r\s]*Load|ResourceLoader[\n\r\s]*\.[\n\r\s]*Load)/.source + genericPartRegex.source + /[\n\r\s]*\([\n\r\s]*(?"(?\w*))?/.source); 24 | const scenePathRegex = /\bChangeScene[\n\r\s]*\([\n\r\s]*(?"(?\w*))?/; 25 | const signalsRegex = /\b(Connect|Disconnect|IsConnected|EmitSignal)[\n\r\s]*\([\n\r\s]*(?"(?\w*))?$/; 26 | const toSignalRegex = new RegExp(/\bToSignal[\n\r\s]*\([\n\r\s]*/.source + qualifiedNameRegex.source + /[\n\r\s]*,[\n\r\s]*(?"(?\w*))?$/.source); 27 | 28 | export class GodotCompletionProvider implements vscode.CompletionItemProvider { 29 | client: Client; 30 | 31 | constructor(client: Client) { 32 | this.client = client; 33 | } 34 | 35 | getPrefixLines(document: vscode.TextDocument, position: vscode.Position): [string, number] { 36 | let result: string = ''; 37 | if (position.line > 1) { 38 | result += document.lineAt(position.line - 2).text + '\n'; 39 | } 40 | if (position.line > 0) { 41 | result += document.lineAt(position.line - 1).text + '\n'; 42 | } 43 | let extraLength = result.length; 44 | result += document.lineAt(position.line).text; 45 | return [result, extraLength + position.character]; 46 | } 47 | 48 | provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): vscode.ProviderResult { 49 | if (!this.client.isConnected() || (!this.client.peer?.isConnected ?? false)) { 50 | return undefined; 51 | } 52 | 53 | let filePath = fixPathForGodot(document.uri.fsPath); 54 | 55 | let [lines, character] = this.getPrefixLines(document, position); 56 | let linePrefix = lines.substr(0, character); 57 | let lineSuffix = lines.substr(character); 58 | 59 | let genericStringItemImpl = (regexes: RegExp[] | RegExp, completionKind: CompletionKind) => 60 | this.genericStringItemImpl(regexes, completionKind, filePath, token, context, linePrefix, lineSuffix); 61 | 62 | return genericStringItemImpl(getNodeRegex, CompletionKind.NodePaths) || 63 | genericStringItemImpl(inputActionRegex, CompletionKind.InputActions) || 64 | genericStringItemImpl(resourcePathRegex, CompletionKind.ResourcePaths) || 65 | genericStringItemImpl(scenePathRegex, CompletionKind.ScenePaths) || 66 | genericStringItemImpl([signalsRegex, toSignalRegex], CompletionKind.Signals) || 67 | undefined; 68 | } 69 | 70 | genericStringItemImpl(regexes: RegExp[] | RegExp, completionKind: CompletionKind, filePath: string, 71 | token: vscode.CancellationToken, context: vscode.CompletionContext, 72 | linePrefix: string, lineSuffix: string): vscode.ProviderResult { 73 | let isMatch = false; 74 | let startsWithQuote = false; 75 | let endsWithQuote = false; 76 | let partialString = ''; 77 | 78 | let match; 79 | 80 | if (regexes instanceof RegExp) { 81 | regexes = [regexes]; 82 | } 83 | 84 | for (let regex in regexes) { 85 | if ((match = linePrefix.match(regexes[regex]))) { 86 | isMatch = true; 87 | if (match.groups?.withQuote !== undefined) { 88 | startsWithQuote = true; 89 | endsWithQuote = lineSuffix.startsWith('"'); 90 | partialString = match.groups.partialString || ''; 91 | } 92 | break; 93 | } 94 | } 95 | 96 | if (isMatch) { 97 | return new Promise(async (resolve, reject) => { 98 | let request = { 99 | Kind: completionKind as number, 100 | ScriptFile: filePath 101 | }; 102 | 103 | const response = await this.client.peer?.sendRequest('CodeCompletion', JSON.stringify(request)); 104 | 105 | if (response === undefined || response.status !== MessageStatus.Ok) { 106 | if (response) { 107 | console.error(`Code completion request failed with status: ${response?.status}`); 108 | } else { 109 | console.error('Code completion request failed'); 110 | } 111 | reject(); 112 | return; 113 | } 114 | 115 | let responseObj = JSON.parse(response.body); 116 | let suggestions: string[] = responseObj.Suggestions; 117 | 118 | let tweak = (str: string) => { 119 | if (startsWithQuote && str.startsWith('"')) { 120 | return str.substr(1, str.length - (endsWithQuote ? 2 : 1)); 121 | } 122 | return str; 123 | }; 124 | 125 | resolve(suggestions.filter(suggestion => suggestion.startsWith('"' + partialString)).map(suggestion => { 126 | let item = new vscode.CompletionItem(suggestion, vscode.CompletionItemKind.Value); 127 | item.insertText = tweak(suggestion); 128 | return item; 129 | })); 130 | }); 131 | } 132 | 133 | return undefined; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "godot-csharp-vscode", 3 | "displayName": "C# Tools for Godot", 4 | "description": "Debugger and utilities for working with Godot C# projects", 5 | "icon": "icon.png", 6 | "version": "0.2.1", 7 | "publisher": "neikeq", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/godotengine/godot-csharp-vscode" 12 | }, 13 | "engines": { 14 | "vscode": "^1.62.0" 15 | }, 16 | "categories": [ 17 | "Debuggers", 18 | "Other" 19 | ], 20 | "activationEvents": [ 21 | "workspaceContains:**/project.godot", 22 | "onDebugResolve:godot", 23 | "onCommand:godot.csharp.selectProject", 24 | "onCommand:godot.csharp.generateAssets", 25 | "onCommand:godot.csharp.getLaunchScene" 26 | ], 27 | "main": "./dist/extension.bundled.js", 28 | "scripts": { 29 | "vscode:prepublish": "make build", 30 | "compile": "make build", 31 | "compile-tsc": "make tsc", 32 | "compile-tsc-debug": "make tsc-debug", 33 | "watch": "tsc -watch -p ./", 34 | "webpack": "webpack --mode production", 35 | "webpack-debug": "webpack --mode development", 36 | "webpack-watch": "webpack --mode development --watch" 37 | }, 38 | "dependencies": { 39 | "async-file": "^2.0.2", 40 | "chokidar": "^3.4.0", 41 | "fs-extra": "^10.0.0", 42 | "jsonc-parser": "^3.0.0", 43 | "lookpath": "^1.2.1", 44 | "promise-socket": "^6.0.3", 45 | "vscode-debugprotocol": "^1.40.0" 46 | }, 47 | "extensionDependencies": [ 48 | "ms-dotnettools.csharp", 49 | "ms-vscode.mono-debug" 50 | ], 51 | "devDependencies": { 52 | "@types/fs-extra": "^9.0.12", 53 | "@types/glob": "^7.1.1", 54 | "@types/mocha": "^5.2.6", 55 | "@types/node": "^16.4.1", 56 | "@types/vscode": "^1.62.0", 57 | "glob": "^7.1.4", 58 | "mocha": "^6.1.4", 59 | "ts-loader": "^7.0.5", 60 | "tslint": "^5.12.1", 61 | "typescript": "^3.3.1", 62 | "vsce": "^1.20.0", 63 | "webpack": "^5.70.0", 64 | "webpack-cli": "^4.9.2" 65 | }, 66 | "breakpoints": [ 67 | { 68 | "language": "csharp" 69 | }, 70 | { 71 | "language": "fsharp" 72 | } 73 | ], 74 | "contributes": { 75 | "configuration": { 76 | "title": "Godot Mono", 77 | "properties": { 78 | "godot.csharp.executablePath": { 79 | "type": "string", 80 | "default": null, 81 | "description": "Path to the Godot engine executable." 82 | } 83 | } 84 | }, 85 | "commands": [ 86 | { 87 | "command": "godot.csharp.generateAssets", 88 | "title": "Generate Assets for Build and Debug", 89 | "category": "C# Godot" 90 | }, 91 | { 92 | "command": "godot.csharp.selectProject", 93 | "title": "Select Project", 94 | "category": "C# Godot" 95 | } 96 | ], 97 | "breakpoints": [ 98 | { 99 | "language": "csharp" 100 | }, 101 | { 102 | "language": "fsharp" 103 | } 104 | ], 105 | "debuggers": [ 106 | { 107 | "type": "godot-mono", 108 | "label": "C# Godot", 109 | "languages": [ 110 | "csharp", 111 | "fsharp" 112 | ], 113 | "program": "./dist/GodotDebugSession/GodotDebugSession.exe", 114 | "osx": { 115 | "runtime": "mono" 116 | }, 117 | "linux": { 118 | "runtime": "mono" 119 | }, 120 | "variables": { 121 | "SelectLaunchScene": "godot.csharp.getLaunchScene" 122 | }, 123 | "configurationSnippets": [ 124 | { 125 | "label": "C# Godot: Play in Editor Configuration", 126 | "description": "Launch a C# Godot App from the open editor with a debugger.", 127 | "body": { 128 | "name": "Play in Editor", 129 | "type": "godot-mono", 130 | "mode": "playInEditor", 131 | "request": "launch" 132 | } 133 | }, 134 | { 135 | "label": "C# Godot: Launch Configuration", 136 | "description": "Launch a C# Godot App with a debugger.", 137 | "body": { 138 | "name": "Launch", 139 | "type": "godot-mono", 140 | "request": "launch", 141 | "mode": "executable", 142 | "preLaunchTask": "build", 143 | "executable": "${1:}", 144 | "executableArguments": [ 145 | "--path", 146 | "${workspaceRoot}" 147 | ] 148 | } 149 | }, 150 | { 151 | "label": "C# Godot: Launch Configuration (Select Scene)", 152 | "description": "Launch a C# Godot App with a debugger.", 153 | "body": { 154 | "name": "Launch (Select Scene)", 155 | "type": "godot-mono", 156 | "request": "launch", 157 | "mode": "executable", 158 | "preLaunchTask": "build", 159 | "executable": "${1:}", 160 | "executableArguments": [ 161 | "--path", 162 | "${workspaceRoot}", 163 | "${command:SelectLaunchScene}" 164 | ] 165 | } 166 | }, 167 | { 168 | "label": "C# Godot: Attach Configuration", 169 | "description": "Attach a debugger to a C# Godot App.", 170 | "body": { 171 | "name": "Attach", 172 | "type": "godot-mono", 173 | "request": "attach", 174 | "address": "localhost", 175 | "port": 23685 176 | } 177 | } 178 | ], 179 | "configurationAttributes": { 180 | "playInEditor": {}, 181 | "launch": { 182 | "properties": { 183 | "executable": { 184 | "type": "string", 185 | "description": "Path to the Godot executable" 186 | }, 187 | "mode": { 188 | "type": "string", 189 | "enum": [ 190 | "playInEditor", 191 | "executable" 192 | ], 193 | "enumDescriptions": [ 194 | "Launches the project in the Godot editor for debugging", 195 | "Launches the project with the specified executable for debugging" 196 | ], 197 | "description": "Launch mode" 198 | }, 199 | "executableArguments": { 200 | "type": "array", 201 | "description": "Arguments to append after the executable" 202 | }, 203 | "env": { 204 | "type": "object", 205 | "description": "Environment variables", 206 | "default": null 207 | } 208 | } 209 | }, 210 | "attach": { 211 | "required": [ 212 | "address", 213 | "port" 214 | ], 215 | "properties": { 216 | "address": { 217 | "type": "string", 218 | "description": "Debugger address to attach to.", 219 | "default": "localhost" 220 | }, 221 | "port": { 222 | "type": "number", 223 | "description": "Debugger port to attach to.", 224 | "default": 23685 225 | } 226 | } 227 | } 228 | } 229 | } 230 | ] 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Client, Peer, MessageContent, MessageStatus, ILogger, IMessageHandler } from './godot-tools-messaging/client'; 3 | import * as completion_provider from './completion-provider'; 4 | import * as debug_provider from './debug-provider'; 5 | import * as assets_provider from './assets-provider'; 6 | import { getWorkspaceScenes } from './workspace-utils'; 7 | import { fixPathForGodot } from './godot-utils'; 8 | import { findProjectFiles, ProjectLocation, promptForProject } from './project-select'; 9 | 10 | export let client: Client; 11 | let codeCompletionProvider: vscode.Disposable; 12 | let debugConfigProvider: vscode.Disposable; 13 | let statusBarItem: vscode.StatusBarItem; 14 | 15 | class Logger implements ILogger { 16 | logDebug(message: string): void { 17 | console.debug(message); 18 | } 19 | logInfo(message: string): void { 20 | console.info(message); 21 | } 22 | logWarning(message: string): void { 23 | console.warn(message); 24 | } 25 | logError(message: string): void; 26 | logError(message: string, e: Error): void; 27 | logError(message: any, e?: any) { 28 | console.error(message, e); 29 | } 30 | } 31 | 32 | type RequestHandler = (peer: Peer, content: MessageContent) => Promise; 33 | 34 | class MessageHandler implements IMessageHandler { 35 | requestHandlers = new Map(); 36 | 37 | constructor() { 38 | this.requestHandlers.set('OpenFile', this.handleOpenFile); 39 | } 40 | 41 | async handleRequest(peer: Peer, id: string, content: MessageContent, logger: ILogger): Promise { 42 | let handler = this.requestHandlers.get(id); 43 | if (handler === undefined) { 44 | logger.logError(`Received unknown request: ${id}`); 45 | return new MessageContent(MessageStatus.RequestNotSupported, 'null'); 46 | } 47 | 48 | let response = await handler(peer, content); 49 | return new MessageContent(response.status, JSON.stringify(response)); 50 | } 51 | 52 | async handleOpenFile(peer: Peer, content: MessageContent): Promise { 53 | // Not used yet by Godot as it doesn't brind the VSCode window to foreground 54 | 55 | let request = JSON.parse(content.body); 56 | 57 | let file: string | undefined = request.File; 58 | let line: number | undefined = request.Line; 59 | let column: number | undefined = request.Column; 60 | 61 | if (file === undefined) { 62 | return new MessageContent(MessageStatus.InvalidRequestBody, 'null'); 63 | } 64 | 65 | let openPath = vscode.Uri.parse(file); 66 | vscode.workspace.openTextDocument(openPath).then(doc => { 67 | vscode.window.showTextDocument(doc); 68 | 69 | if (line !== undefined) { 70 | line -= 1; 71 | 72 | if (column !== undefined) { 73 | column -= 1; 74 | } 75 | 76 | let editor = vscode.window.activeTextEditor; 77 | 78 | if (editor !== undefined) { 79 | const position = editor.selection.active; 80 | 81 | let newPosition = position.with(line, column); 82 | let range = new vscode.Range(newPosition, newPosition); 83 | editor.selection = new vscode.Selection(range.start, range.end); 84 | editor.revealRange(range); 85 | } 86 | } 87 | }); 88 | 89 | return new MessageContent(MessageStatus.Ok, '{}'); 90 | } 91 | } 92 | 93 | export async function activate(context: vscode.ExtensionContext) { 94 | const foundProjects: ProjectLocation[] = await findProjectFiles(); 95 | // No project.godot files found. The extension doesn't need to do anything more. 96 | if (foundProjects.length === 0) { 97 | return; 98 | } 99 | 100 | // Setup the status bar / project selector and prompt for project if necessary 101 | const commandId = 'godot.csharp.selectProject'; 102 | statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); 103 | statusBarItem.command = commandId; 104 | statusBarItem.show(); 105 | context.subscriptions.push(statusBarItem); 106 | context.subscriptions.push(vscode.commands.registerCommand(commandId, async () => { 107 | const project = await promptForProject(); // project.godot 108 | if (project !== undefined) { 109 | setupProject(project, context); 110 | } 111 | })); 112 | 113 | // One project.godot files found. Use it. 114 | if (foundProjects.length === 1) { 115 | setupProject(foundProjects[0], context); 116 | } 117 | // Multiple project.godot files found. Prompt the user for which one they want to use. 118 | else { 119 | const project = await promptForProject(); 120 | if (project !== undefined) { 121 | setupProject(project, context); 122 | } 123 | } 124 | 125 | // Setup generate assets command 126 | const generateAssetsCommand = vscode.commands.registerCommand('godot.csharp.generateAssets', async () => { 127 | await assets_provider.addAssets(); 128 | }); 129 | context.subscriptions.push(generateAssetsCommand); 130 | 131 | // Setup get launch scene command 132 | const getLaunchSceneCommand = vscode.commands.registerCommand('godot.csharp.getLaunchScene', () => { 133 | return vscode.window.showQuickPick(getWorkspaceScenes(client?.getGodotProjectDir())); 134 | }); 135 | context.subscriptions.push(getLaunchSceneCommand); 136 | } 137 | 138 | function setupProject(project: ProjectLocation, context: vscode.ExtensionContext) { 139 | const statusBarPath: string = project.relativeProjectPath === '.' ? './' : project.relativeProjectPath; 140 | statusBarItem.text = `$(folder) Godot Project: ${statusBarPath}`; 141 | // Setup client 142 | if (client !== undefined) { 143 | client.dispose(); 144 | } 145 | client = new Client( 146 | 'VisualStudioCode', 147 | fixPathForGodot(project.absoluteProjectPath), 148 | new MessageHandler(), 149 | new Logger(), 150 | ); 151 | client.start(); 152 | 153 | // Setup debug provider 154 | if (debugConfigProvider !== undefined) { 155 | debugConfigProvider.dispose(); 156 | } 157 | debugConfigProvider = vscode.debug.registerDebugConfigurationProvider( 158 | 'godot-mono', 159 | new debug_provider.GodotMonoDebugConfigProvider(project.absoluteProjectPath) 160 | ); 161 | context.subscriptions.push(debugConfigProvider); 162 | 163 | // Setup completion provider 164 | // There's no way to extend OmniSharp without having to provide our own language server. 165 | // That will be a big task so for now we will provide this basic completion provider. 166 | if (codeCompletionProvider !== undefined) { 167 | codeCompletionProvider.dispose(); 168 | } 169 | // Create client, create provider, register and subscribe provider 170 | codeCompletionProvider = vscode.languages.registerCompletionItemProvider( 171 | 'csharp', new completion_provider.GodotCompletionProvider(client), 172 | // Trigger characters 173 | '(', '"', ',', ' ' 174 | ); 175 | context.subscriptions.push(codeCompletionProvider); 176 | } 177 | 178 | export function deactivate() { 179 | client.dispose(); 180 | } 181 | 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# Tools for Godot 2 | 3 | > [!WARNING] 4 | > 5 | > **This extension currently does not support Godot 4.x.** 6 | > 7 | > However, you can still debug Godot C# projects in Visual Studio Code by installing 8 | > [Microsoft's C# extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp), 9 | > then configuring it as described in the 10 | > [documentation](https://docs.godotengine.org/en/4.0/tutorials/scripting/c_sharp/c_sharp_basics.html#visual-studio-code). 11 | 12 | Debugger and utilities for working with Godot C# projects in VSCode. 13 | 14 | ## Requirements 15 | 16 | **Godot >= 3.2.2 and < 4.0**. Older versions of Godot or **4.0** and higher are not supported. 17 | 18 | ## Features 19 | 20 | - Debugging. 21 | - Launch a game directly in the Godot editor from VSCode. 22 | - Additional code completion for Node paths, Input actions, Resource paths, Scene paths and Signal names. 23 | 24 | **NOTES:** 25 | - A running Godot instance must be editing the project in order for `Play in Editor` and the code completion to work. 26 | - Node path suggestions are provided from the currently edited scene in the Godot editor. 27 | - Currently Signal suggestions are only provided using the information from the project build 28 | results, not from the information in the edited document. This will change in the future. 29 | 30 | ## VSCode installation and configuration 31 | 32 | Install via extensions marketplace by searching for [`neikeq.godot-csharp-vscode`](https://marketplace.visualstudio.com/items?itemName=neikeq.godot-csharp-vscode). 33 | 34 | ### Multiple Project setup 35 | 36 | If the current workspace contains multiple Godot projects, the extension will prompt you to select the one you want to use with the extension on opening the workspace in VSCode. The selected project can be changed anytime from the status bar or using the `Select Project` command. 37 | 38 | - **Option 1.** Using the status bar: 39 | - Click on the Godot project status bar item.\ 40 | ![Select Project on the status bar](images/setupProjectStatusbar.png) 41 | - Select the Godot project you want to use. 42 | 43 | - **Option 2.** Using the `Select Project` command: 44 | - Open the Command Palette (Ctrl + P). 45 | - Select `C# Godot: Select Project`.\ 46 | ![Select the Select Project command](images/setupProjectCommand.png) 47 | - Select the Godot project you want to use. 48 | 49 | ### Setup debugging 50 | 51 | To debug a Godot project you have to create the [debugger launch configurations](#debugger-launch-configurations). It can be created from the Debug panel or by using the `Generate Assets for Build and Debug` command. 52 | 53 | - **Option 1.** Using the `Generate Assets for Build and Debug` command: 54 | - Open the Command Palette (Ctrl + P). 55 | - Select `C# Godot: Generate Assets for Build and Debug`.\ 56 | ![Select the Generate Assets for Build and Debug command](images/setupDebuggingCommand.png) 57 | - If debugger configurations already exist, you will be prompted if you want to override them. 58 | - It will have generated the `launch.json` and `tasks.json` files in the `.vscode` directory.\ 59 | See the [debugger launch configurations](#debugger-launch-configurations), some configurations 60 | may require more setup. 61 | 62 | - **Option 2.** From the **Debug panel**: 63 | - If debugger configurations already exist, remove them or use the 64 | `Generate Assets for Build and Debug` command to override them. 65 | - Click on `create a launch.json file`.\ 66 | ![Create launch.json file](images/setupDebuggingCreate.png) 67 | - Select `C# Godot`.\ 68 | ![Select C# Godot](images/setupDebuggingSelectGodot.png) 69 | - It will have generated the `launch.json` and `tasks.json` files in the `.vscode` directory.\ 70 | See the [debugger launch configurations](#debugger-launch-configurations), some configurations 71 | may require more setup. 72 | 73 | ### Additional `Launch` configuration 74 | 75 | The `Launch` [debugger configurations](#debugger-launch-configurations) requires additional setup. The _"executable"_ property must be set to a path that points to the Godot executable that will be launched. By default, the extension tries to automatically populate this property with the path to the running Godot instance but if there isn't one it needs to be set manually: 76 | ![Fix editor path](images/setupDebuggingGodotPath.png) 77 | 78 | You can also set the `godot.csharp.executablePath` setting to the path that points to the Godot executable that will always be used when generating the debugger configurations so you won't have to set it manually everytime. 79 | 80 | The [generated debugger configuration](#setup-debugging) will also create a `tasks.json` that contains a build task for building the project from VSCode which is used by the `Launch` configuration in order to build the project before launching (this is configured in the _"preLaunchTask"_ property of the configuration and can be removed). 81 | 82 | The build task uses the Godot executable to build the the C# project (this is configured in the _"command"_ property and must be configured like the _"executable"_ property of the `Launch` configuration if the extension could not find the right path). The build task can be modified to execute the `dotnet` command directly instead or modify the [Godot CLI arguments](https://docs.godotengine.org/en/3.4/getting_started/editor/command_line_tutorial.html#command-line-reference). 83 | 84 | ## Debugger launch configurations 85 | 86 | By default the extension creates the following launch configurations: 87 | 88 | - **Play in Editor**\ 89 | Launches the game in the Godot editor for debugging in VSCode.\ 90 | For this option to work, a running Godot instance must be editing the project. 91 | - **Launch**\ 92 | Launches the game with a Godot executable for debugging in VSCode.\ 93 | This option requires the value of the _"executable"_ property to be set to 94 | a path that points to the Godot executable that will be launched.\ 95 | The `godot.csharp.executablePath` setting can be configured to automatically populate the 96 | executable property with its value, if not configured it will be populated with the path 97 | to the running Godot instance if there is one, otherwise it will have to be populated manually. 98 | See [additional `Launch` configuration](#additional-launch-configuration). 99 | - **Launch (Select Scene)**\ 100 | Launches the game with a Godot executable for debugging in VSCode, allowing the user 101 | to select which scene to run on every execution.\ 102 | This option works just like the `Launch` option and also requires the value 103 | of the _"executable"_ property to be set. 104 | See [additional `Launch` configuration](#additional-launch-configuration). 105 | - **Attach**\ 106 | Attaches to a running Godot instance that was configured to listen for a debugger connection. 107 | 108 | ## Screenshots 109 | 110 | ![Debugging](images/debugging.png) 111 | 112 | ![Nodes code completion](images/codeCompletionNodePaths.png) 113 | 114 | ![Input actions code completion](images/codeCompletionInputActions.png) 115 | 116 | ![Signals code completion](images/codeCompletionSignals.png) 117 | -------------------------------------------------------------------------------- /GodotDebugSession/GodotDebuggerSession.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net.Sockets; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using GodotTools.IdeMessaging.Requests; 7 | using Medallion.Shell; 8 | using Mono.Debugging.Client; 9 | using Mono.Debugging.Soft; 10 | using System.Collections.Generic; 11 | 12 | namespace GodotDebugSession 13 | { 14 | public class GodotDebuggerSession : SoftDebuggerSession 15 | { 16 | private bool _attached; 17 | private NetworkStream _godotRemoteDebuggerStream; 18 | 19 | private class GodotMessagingLogger : GodotTools.IdeMessaging.ILogger 20 | { 21 | public void LogDebug(string message) => Logger.Log(message); 22 | 23 | public void LogInfo(string message) => Logger.Log(message); 24 | 25 | public void LogWarning(string message) => Logger.Log(message); 26 | 27 | public void LogError(string message) => Logger.LogError(message); 28 | 29 | public void LogError(string message, Exception e) => Logger.LogError(message, e); 30 | } 31 | 32 | protected override async void OnRun(DebuggerStartInfo startInfo) 33 | { 34 | var godotStartInfo = (GodotDebuggerStartInfo)startInfo; 35 | 36 | switch (godotStartInfo.ExecutionType) 37 | { 38 | case ExecutionType.PlayInEditor: 39 | { 40 | try 41 | { 42 | _attached = false; 43 | StartListening(godotStartInfo, out var assignedDebugPort); 44 | 45 | string host = "127.0.0.1"; 46 | string godotProjectDir = startInfo.WorkingDirectory; 47 | 48 | using (var messagingClient = new GodotTools.IdeMessaging.Client( 49 | identity: "VSCodeGodotDebugSession", godotProjectDir, 50 | new GodotMessageHandler(), new GodotMessagingLogger())) 51 | { 52 | messagingClient.Start(); 53 | await messagingClient.AwaitConnected(); 54 | var response = await messagingClient.SendRequest( 55 | new DebugPlayRequest { DebuggerHost = host, DebuggerPort = assignedDebugPort }); 56 | 57 | if (response.Status != GodotTools.IdeMessaging.MessageStatus.Ok) 58 | { 59 | Logger.Log("Debug play request failed."); 60 | Exit(); 61 | // ReSharper disable once RedundantJumpStatement 62 | return; 63 | } 64 | } 65 | 66 | // TODO: Read the editor player stdout and stderr somehow 67 | } 68 | catch (Exception e) 69 | { 70 | Logger.LogError(e); 71 | } 72 | 73 | break; 74 | } 75 | case ExecutionType.Launch: 76 | { 77 | try 78 | { 79 | _attached = false; 80 | StartListening(godotStartInfo, out var assignedDebugPort); 81 | 82 | // Listener to replace the Godot editor remote debugger. 83 | // We use it to notify the game when assemblies should be reloaded. 84 | var remoteDebugListener = new TcpListener(IPAddress.Any, 0); 85 | remoteDebugListener.Start(); 86 | _ = remoteDebugListener.AcceptTcpClientAsync().ContinueWith(OnGodotRemoteDebuggerConnected); 87 | 88 | string workingDir = startInfo.WorkingDirectory; 89 | string host = "127.0.0.1"; 90 | int remoteDebugPort = ((IPEndPoint)remoteDebugListener.LocalEndpoint).Port; 91 | 92 | // Launch Godot to run the game and connect to our remote debugger 93 | 94 | var args = new List() 95 | { 96 | "--path", workingDir, 97 | "--remote-debug", $"{host}:{remoteDebugPort}", 98 | }; 99 | args.AddRange(godotStartInfo.ExecutableArguments); 100 | 101 | var process = Command.Run(godotStartInfo.GodotExecutablePath, args, options => 102 | { 103 | options.WorkingDirectory(workingDir); 104 | options.ThrowOnError(true); 105 | 106 | // Tells Godot to connect to the mono debugger we just started 107 | options.EnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", 108 | "--debugger-agent=transport=dt_socket" + 109 | $",address={host}:{assignedDebugPort}" + 110 | ",server=n"); 111 | }) 112 | .RedirectTo(godotStartInfo.ProcessOutputListener.GetStdOutTextWriter()) 113 | .RedirectStandardErrorTo(godotStartInfo.ProcessOutputListener.GetStdErrTextWriter()); 114 | 115 | try 116 | { 117 | await process.Task; 118 | } 119 | catch (Exception e) 120 | { 121 | Logger.Log($"Godot launch request failed: {e.Message}."); 122 | Exit(); 123 | return; 124 | } 125 | 126 | OnDebuggerOutput(false, $"Godot PID:{process.ProcessId}{Environment.NewLine}"); 127 | } 128 | catch (Exception e) 129 | { 130 | Logger.LogError(e); 131 | } 132 | 133 | break; 134 | } 135 | case ExecutionType.Attach: 136 | { 137 | _attached = true; 138 | StartConnecting(godotStartInfo); 139 | break; 140 | } 141 | default: 142 | throw new NotImplementedException(godotStartInfo.ExecutionType.ToString()); 143 | } 144 | } 145 | 146 | private async Task OnGodotRemoteDebuggerConnected(Task task) 147 | { 148 | var tcpClient = task.Result; 149 | _godotRemoteDebuggerStream = tcpClient.GetStream(); 150 | byte[] buffer = new byte[1000]; 151 | while (tcpClient.Connected) 152 | { 153 | // There is no library to decode this messages, so 154 | // we just pump buffer so it doesn't go out of memory 155 | var readBytes = await _godotRemoteDebuggerStream.ReadAsync(buffer, 0, buffer.Length); 156 | } 157 | } 158 | 159 | protected override void OnExit() 160 | { 161 | if (_attached) 162 | base.OnDetach(); 163 | else 164 | base.OnExit(); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /GodotDebugSession/.gitignore: -------------------------------------------------------------------------------- 1 | # Rider 2 | .idea/ 3 | 4 | # Visual Studio Code 5 | .vscode/ 6 | 7 | ## Ignore Visual Studio temporary files, build results, and 8 | ## files generated by popular Visual Studio add-ons. 9 | ## 10 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 11 | 12 | # User-specific files 13 | *.rsuser 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Mono auto generated files 23 | mono_crash.* 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # JustCode is a .NET coding add-in 136 | .JustCode 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Visual Studio code coverage results 149 | *.coverage 150 | *.coveragexml 151 | 152 | # NCrunch 153 | _NCrunch_* 154 | .*crunch*.local.xml 155 | nCrunchTemp_* 156 | 157 | # MightyMoose 158 | *.mm.* 159 | AutoTest.Net/ 160 | 161 | # Web workbench (sass) 162 | .sass-cache/ 163 | 164 | # Installshield output folder 165 | [Ee]xpress/ 166 | 167 | # DocProject is a documentation generator add-in 168 | DocProject/buildhelp/ 169 | DocProject/Help/*.HxT 170 | DocProject/Help/*.HxC 171 | DocProject/Help/*.hhc 172 | DocProject/Help/*.hhk 173 | DocProject/Help/*.hhp 174 | DocProject/Help/Html2 175 | DocProject/Help/html 176 | 177 | # Click-Once directory 178 | publish/ 179 | 180 | # Publish Web Output 181 | *.[Pp]ublish.xml 182 | *.azurePubxml 183 | # Note: Comment the next line if you want to checkin your web deploy settings, 184 | # but database connection strings (with potential passwords) will be unencrypted 185 | *.pubxml 186 | *.publishproj 187 | 188 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 189 | # checkin your Azure Web App publish settings, but sensitive information contained 190 | # in these scripts will be unencrypted 191 | PublishScripts/ 192 | 193 | # NuGet Packages 194 | *.nupkg 195 | # NuGet Symbol Packages 196 | *.snupkg 197 | # The packages folder can be ignored because of Package Restore 198 | **/[Pp]ackages/* 199 | # except build/, which is used as an MSBuild target. 200 | !**/[Pp]ackages/build/ 201 | # Uncomment if necessary however generally it will be regenerated when needed 202 | #!**/[Pp]ackages/repositories.config 203 | # NuGet v3's project.json files produces more ignorable files 204 | *.nuget.props 205 | *.nuget.targets 206 | 207 | # Microsoft Azure Build Output 208 | csx/ 209 | *.build.csdef 210 | 211 | # Microsoft Azure Emulator 212 | ecf/ 213 | rcf/ 214 | 215 | # Windows Store app package directories and files 216 | AppPackages/ 217 | BundleArtifacts/ 218 | Package.StoreAssociation.xml 219 | _pkginfo.txt 220 | *.appx 221 | *.appxbundle 222 | *.appxupload 223 | 224 | # Visual Studio cache files 225 | # files ending in .cache can be ignored 226 | *.[Cc]ache 227 | # but keep track of directories ending in .cache 228 | !?*.[Cc]ache/ 229 | 230 | # Others 231 | ClientBin/ 232 | ~$* 233 | *~ 234 | *.dbmdl 235 | *.dbproj.schemaview 236 | *.jfm 237 | *.pfx 238 | *.publishsettings 239 | orleans.codegen.cs 240 | 241 | # Including strong name files can present a security risk 242 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 243 | #*.snk 244 | 245 | # Since there are multiple workflows, uncomment next line to ignore bower_components 246 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 247 | #bower_components/ 248 | 249 | # RIA/Silverlight projects 250 | Generated_Code/ 251 | 252 | # Backup & report files from converting an old project file 253 | # to a newer Visual Studio version. Backup files are not needed, 254 | # because we have git ;-) 255 | _UpgradeReport_Files/ 256 | Backup*/ 257 | UpgradeLog*.XML 258 | UpgradeLog*.htm 259 | ServiceFabricBackup/ 260 | *.rptproj.bak 261 | 262 | # SQL Server files 263 | *.mdf 264 | *.ldf 265 | *.ndf 266 | 267 | # Business Intelligence projects 268 | *.rdl.data 269 | *.bim.layout 270 | *.bim_*.settings 271 | *.rptproj.rsuser 272 | *- [Bb]ackup.rdl 273 | *- [Bb]ackup ([0-9]).rdl 274 | *- [Bb]ackup ([0-9][0-9]).rdl 275 | 276 | # Microsoft Fakes 277 | FakesAssemblies/ 278 | 279 | # GhostDoc plugin setting file 280 | *.GhostDoc.xml 281 | 282 | # Node.js Tools for Visual Studio 283 | .ntvs_analysis.dat 284 | node_modules/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # CodeRush personal settings 311 | .cr/personal 312 | 313 | # Python Tools for Visual Studio (PTVS) 314 | __pycache__/ 315 | *.pyc 316 | 317 | # Cake - Uncomment if you are using it 318 | # tools/** 319 | # !tools/packages.config 320 | 321 | # Tabs Studio 322 | *.tss 323 | 324 | # Telerik's JustMock configuration file 325 | *.jmconfig 326 | 327 | # BizTalk build output 328 | *.btp.cs 329 | *.btm.cs 330 | *.odx.cs 331 | *.xsd.cs 332 | 333 | # OpenCover UI analysis results 334 | OpenCover/ 335 | 336 | # Azure Stream Analytics local run output 337 | ASALocalRun/ 338 | 339 | # MSBuild Binary and Structured Log 340 | *.binlog 341 | 342 | # NVidia Nsight GPU debugger configuration file 343 | *.nvuser 344 | 345 | # MFractors (Xamarin productivity tool) working folder 346 | .mfractor/ 347 | 348 | # Local History for Visual Studio 349 | .localhistory/ 350 | 351 | # BeatPulse healthcheck temp database 352 | healthchecksdb 353 | 354 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 355 | MigrationBackup/ 356 | -------------------------------------------------------------------------------- /typings/vscode-tasks.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // Copied from http://code.visualstudio.com/docs/editor/tasks_appendix 7 | 8 | declare module "vscode-tasks" { 9 | export interface TaskConfiguration extends BaseTaskConfiguration { 10 | 11 | /** 12 | * The configuration's version number 13 | */ 14 | version: string; 15 | 16 | /** 17 | * Windows specific task configuration 18 | */ 19 | windows?: BaseTaskConfiguration; 20 | 21 | /** 22 | * Mac specific task configuration 23 | */ 24 | osx?: BaseTaskConfiguration; 25 | 26 | /** 27 | * Linux specific task configuration 28 | */ 29 | linux?: BaseTaskConfiguration; 30 | } 31 | 32 | export interface BaseTaskConfiguration { 33 | 34 | /** 35 | * The type of a custom task. Tasks of type "shell" are executed 36 | * inside a shell (e.g. bash, cmd, powershell, ...) 37 | */ 38 | type?: "shell" | "process"; 39 | 40 | /** 41 | * The command to be executed. Can be an external program or a shell 42 | * command. 43 | */ 44 | command?: string; 45 | 46 | /** 47 | * Specifies whether a global command is a background task. 48 | */ 49 | isBackground?: boolean; 50 | 51 | /** 52 | * The command options used when the command is executed. Can be omitted. 53 | */ 54 | options?: CommandOptions; 55 | 56 | /** 57 | * The arguments passed to the command. Can be omitted. 58 | */ 59 | args?: string[]; 60 | 61 | /** 62 | * The presentation options. 63 | */ 64 | presentation?: PresentationOptions; 65 | 66 | /** 67 | * The problem matcher to be used if a global command is executed (e.g. no tasks 68 | * are defined). A tasks.json file can either contain a global problemMatcher 69 | * property or a tasks property but not both. 70 | */ 71 | problemMatcher?: string | ProblemMatcher | (string | ProblemMatcher)[]; 72 | 73 | /** 74 | * The configuration of the available tasks. A tasks.json file can either 75 | * contain a global problemMatcher property or a tasks property but not both. 76 | */ 77 | tasks?: TaskDescription[]; 78 | } 79 | 80 | 81 | /** 82 | * Options to be passed to the external program or shell 83 | */ 84 | export interface CommandOptions { 85 | 86 | /** 87 | * The current working directory of the executed program or shell. 88 | * If omitted Ticino's current workspace root is used. 89 | */ 90 | cwd?: string; 91 | 92 | /** 93 | * The environment of the executed program or shell. If omitted 94 | * the parent process' environment is used. 95 | */ 96 | env?: { [key: string]: string; }; 97 | 98 | /** 99 | * Configuration of the shell when task type is `shell` 100 | */ 101 | shell: { 102 | 103 | /** 104 | * The shell to use. 105 | */ 106 | executable: string; 107 | 108 | /** 109 | * The arguments to be passed to the shell executable to run in command mode 110 | * (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe). 111 | */ 112 | args?: string[]; 113 | } 114 | } 115 | 116 | /** 117 | * The description of a task. 118 | */ 119 | export interface TaskDescription { 120 | 121 | /** 122 | * The task's name 123 | */ 124 | label: string; 125 | 126 | /** 127 | * The type of a custom task. Tasks of type "shell" are executed 128 | * inside a shell (e.g. bash, cmd, powershell, ...) 129 | */ 130 | type: "shell" | "process"; 131 | 132 | /** 133 | * The command to execute. If the type is "shell" it should be the full 134 | * command line including any additional arguments passed to the command. 135 | */ 136 | command: string; 137 | 138 | /** 139 | * Whether the executed command is kept alive and runs in the background. 140 | */ 141 | isBackground?: boolean; 142 | 143 | /** 144 | * Additional arguments passed to the command. Should be used if type 145 | * is "process". 146 | */ 147 | args?: string[]; 148 | 149 | /** 150 | * Tasks V1 isBuildCommand is used to detect if the tasks is in a build group. 151 | */ 152 | isBuildCommand?: boolean; 153 | 154 | /** 155 | * Defines the group to which this tasks belongs 156 | */ 157 | group?: "build" | "string"; 158 | 159 | /** 160 | * The presentation options. 161 | */ 162 | presentation?: PresentationOptions; 163 | 164 | /** 165 | * The problem matcher(s) to use to capture problems in the tasks 166 | * output. 167 | */ 168 | problemMatcher?: string | ProblemMatcher | (string | ProblemMatcher)[]; 169 | } 170 | 171 | export interface PresentationOptions { 172 | 173 | /** 174 | * Controls whether the task output is reveal in the user interface. 175 | * Defaults to `always`. 176 | */ 177 | reveal?: "never" | "silent" | "always"; 178 | 179 | /** 180 | * Controls whether the command associated with the task is echoed 181 | * in the user interface. 182 | */ 183 | echo?: boolean; 184 | 185 | /** 186 | * Controls whether the panel showing the task output is taking focus. 187 | */ 188 | focus?: boolean; 189 | 190 | /** 191 | * Controls if the task panel is used for this task only (dedicated), 192 | * shared between tasks (shared) or if a new panel is created on 193 | * every task execution (new). Defaults to `shared` 194 | */ 195 | panel?: "shared" | "dedicated" | "new"; 196 | } 197 | 198 | /** 199 | * A description of a problem matcher that detects problems 200 | * in build output. 201 | */ 202 | export interface ProblemMatcher { 203 | 204 | /** 205 | * The name of a base problem matcher to use. If specified the 206 | * base problem matcher will be used as a template and properties 207 | * specified here will replace properties of the base problem 208 | * matcher 209 | */ 210 | base?: string; 211 | 212 | /** 213 | * The owner of the produced VS Code problem. This is typically 214 | * the identifier of a VS Code language service if the problems are 215 | * to be merged with the one produced by the language service 216 | * or 'external'. Defaults to 'external' if omitted. 217 | */ 218 | owner?: string; 219 | 220 | /** 221 | * The severity of the VS Code problem produced by this problem matcher. 222 | * 223 | * Valid values are: 224 | * "error": to produce errors. 225 | * "warning": to produce warnings. 226 | * "info": to produce infos. 227 | * 228 | * The value is used if a pattern doesn't specify a severity match group. 229 | * Defaults to "error" if omitted. 230 | */ 231 | severity?: string; 232 | 233 | /** 234 | * Defines how filename reported in a problem pattern 235 | * should be read. Valid values are: 236 | * - "absolute": the filename is always treated absolute. 237 | * - "relative": the filename is always treated relative to 238 | * the current working directory. This is the default. 239 | * - ["relative", "path value"]: the filename is always 240 | * treated relative to the given path value. 241 | */ 242 | fileLocation?: string | string[]; 243 | 244 | /** 245 | * The name of a predefined problem pattern, the inline definition 246 | * of a problem pattern or an array of problem patterns to match 247 | * problems spread over multiple lines. 248 | */ 249 | pattern?: string | ProblemPattern | ProblemPattern[]; 250 | 251 | /** 252 | * Additional information used to detect when a background task (like a watching task in Gulp) 253 | * is active. 254 | */ 255 | background?: BackgroundMatcher; 256 | } 257 | 258 | /** 259 | * A description to track the start and end of a background task. 260 | */ 261 | export interface BackgroundMatcher { 262 | 263 | /** 264 | * If set to true the watcher is in active mode when the task 265 | * starts. This is equals of issuing a line that matches the 266 | * beginPattern. 267 | */ 268 | activeOnStart?: boolean; 269 | 270 | /** 271 | * If matched in the output the start of a background task is signaled. 272 | */ 273 | beginsPattern?: string; 274 | 275 | /** 276 | * If matched in the output the end of a background task is signaled. 277 | */ 278 | endsPattern?: string; 279 | } 280 | 281 | export interface ProblemPattern { 282 | 283 | /** 284 | * The regular expression to find a problem in the console output of an 285 | * executed task. 286 | */ 287 | regexp: string; 288 | 289 | /** 290 | * The match group index of the filename. 291 | */ 292 | file: number; 293 | 294 | /** 295 | * The match group index of the problems's location. Valid location 296 | * patterns are: (line), (line,column) and (startLine,startColumn,endLine,endColumn). 297 | * If omitted the line and column properties are used. 298 | */ 299 | location?: number; 300 | 301 | /** 302 | * The match group index of the problem's line in the source file. 303 | * Can only be omitted if location is specified. 304 | */ 305 | line?: number; 306 | 307 | /** 308 | * The match group index of the problem's column in the source file. 309 | */ 310 | column?: number; 311 | 312 | /** 313 | * The match group index of the problem's end line in the source file. 314 | * 315 | * Defaults to undefined. No end line is captured. 316 | */ 317 | endLine?: number; 318 | 319 | /** 320 | * The match group index of the problem's end column in the source file. 321 | * 322 | * Defaults to undefined. No end column is captured. 323 | */ 324 | endColumn?: number; 325 | 326 | /** 327 | * The match group index of the problem's severity. 328 | * 329 | * Defaults to undefined. In this case the problem matcher's severity 330 | * is used. 331 | */ 332 | severity?: number; 333 | 334 | /** 335 | * The match group index of the problem's code. 336 | * 337 | * Defaults to undefined. No code is captured. 338 | */ 339 | code?: number; 340 | 341 | /** 342 | * The match group index of the message. Defaults to 0. 343 | */ 344 | message: number; 345 | 346 | /** 347 | * Specifies if the last pattern in a multi line problem matcher should 348 | * loop as long as it does match a line consequently. Only valid on the 349 | * last problem pattern in a multi line problem matcher. 350 | */ 351 | loop?: boolean; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/godot-tools-messaging/client.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as chokidar from 'chokidar'; 5 | import PromiseSocket from 'promise-socket'; 6 | import { Disposable } from 'vscode'; 7 | 8 | async function timeout(ms: number) { 9 | return new Promise(resolve => { 10 | setTimeout(resolve, ms); 11 | }); 12 | } 13 | 14 | class CustomSocket extends PromiseSocket { 15 | buffer: string = ''; 16 | 17 | constructor() { 18 | super(); 19 | this.setEncoding('utf-8'); 20 | } 21 | 22 | async readLine(): Promise { 23 | let chunk: Buffer | string | undefined; 24 | 25 | do { 26 | if (chunk !== undefined) { 27 | if (chunk instanceof Buffer) { 28 | this.buffer += chunk.toString('utf-8'); 29 | } else { 30 | this.buffer += chunk; 31 | } 32 | } 33 | 34 | let indexOfLineBreak = this.buffer.indexOf('\n'); 35 | 36 | if (indexOfLineBreak >= 0) { 37 | let hasCR = indexOfLineBreak !== 0 && this.buffer.charAt(indexOfLineBreak - 1) === '\r'; 38 | let line = this.buffer.substring(0, hasCR ? indexOfLineBreak - 1 : indexOfLineBreak); 39 | this.buffer = this.buffer.substring(indexOfLineBreak + 1); 40 | return line; 41 | } 42 | } 43 | while ((chunk = await this.read()) !== undefined); 44 | 45 | return undefined; 46 | } 47 | 48 | async writeLine(line: string): Promise { 49 | return await this.write(line + '\n', 'utf-8'); 50 | } 51 | } 52 | 53 | type ResponseListener = (response: MessageContent) => void; 54 | 55 | export class Peer implements Disposable { 56 | static readonly protocolVersionMajor: number = 1; 57 | static readonly protocolVersionMinor: number = 1; 58 | static readonly protocolVersionRevision: number = 0; 59 | 60 | static readonly clientHandshakeName: string = 'GodotIdeClient'; 61 | static readonly serverHandshakeName: string = 'GodotIdeServer'; 62 | 63 | socket: CustomSocket; 64 | handshake: IHandshake; 65 | messageHandler: IMessageHandler; 66 | logger: ILogger; 67 | 68 | remoteIdentity?: string; 69 | isConnected: boolean = false; 70 | 71 | requestAwaiterQueues = new Map(); 72 | 73 | constructor(socket: CustomSocket, handshake: IHandshake, messageHandler: IMessageHandler, logger: ILogger) { 74 | this.socket = socket; 75 | this.handshake = handshake; 76 | this.messageHandler = messageHandler; 77 | this.logger = logger; 78 | } 79 | 80 | dispose() { 81 | this.socket?.destroy(); 82 | } 83 | 84 | async process(): Promise { 85 | let decoder = new MessageDecoder(); 86 | 87 | let messageLine: string | undefined; 88 | while ((messageLine = await this.socket.readLine()) !== undefined) { 89 | let [state, msg] = decoder.decode(messageLine); 90 | 91 | if (state === MessageDecoderState.Decoding) { 92 | continue; // Not finished decoding yet 93 | } 94 | 95 | if (state === MessageDecoderState.Errored || msg === undefined) { 96 | this.logger.logError(`Received message line with invalid format: ${messageLine}`); 97 | continue; 98 | } 99 | 100 | this.logger.logDebug(`Received message: ${msg.toString()}`); 101 | 102 | if (msg.kind === MessageKind.Request) { 103 | let responseContent = await this.messageHandler 104 | .handleRequest(this, msg.id, msg.content, this.logger); 105 | await this.writeMessage(new Message(MessageKind.Response, msg.id, responseContent)); 106 | } else if (msg.kind === MessageKind.Response) { 107 | let responseAwaiter: ResponseListener | undefined; 108 | let queue = this.requestAwaiterQueues.get(msg.id); 109 | if (queue === undefined || (responseAwaiter = queue.shift()) === undefined) { 110 | this.logger.logError(`Received unexpected response: ${msg.id}`); 111 | return; 112 | } 113 | responseAwaiter(msg.content); 114 | } else { 115 | this.logger.logError(`Invalid message kind ${MessageKind[msg.kind]}`); 116 | return; 117 | } 118 | } 119 | 120 | this.isConnected = false; 121 | } 122 | 123 | async doHandshake(identity: string): Promise { 124 | if (await this.socket.writeLine(this.handshake.getHandshakeLine(identity)) === 0) { 125 | this.logger.logError('Could not write handshake'); 126 | return false; 127 | } 128 | 129 | let handshakeReceived = false; 130 | 131 | let readHandshakeImpl = async (): Promise => { 132 | let result = await this.socket.readLine(); 133 | handshakeReceived = true; 134 | return result; 135 | }; 136 | let readHandshakePromise = readHandshakeImpl(); 137 | 138 | let maybePeerHandshake = await Promise.race([readHandshakePromise, timeout(8000)]); 139 | 140 | if (!handshakeReceived || maybePeerHandshake === undefined || typeof maybePeerHandshake !== 'string') { 141 | this.logger.logError('Timeout waiting for the client handshake'); 142 | return false; 143 | } 144 | 145 | let peerHandshake = maybePeerHandshake as string; 146 | 147 | let [valid, remoteIdentity] = this.handshake.isValidPeerHandshake(peerHandshake, this.logger); 148 | 149 | if (!valid || remoteIdentity === undefined) { 150 | this.logger.logError('Received invalid handshake: ' + peerHandshake); 151 | return false; 152 | } 153 | 154 | this.remoteIdentity = remoteIdentity; 155 | 156 | this.isConnected = true; 157 | 158 | this.logger.logInfo('Peer connection started'); 159 | 160 | return true; 161 | } 162 | 163 | private async writeMessage(message: Message): Promise { 164 | this.logger.logDebug(`Sending message: ${message.toString()}`); 165 | 166 | let bodyLineCount = message.content.body.match(/[^\n]*\n[^\n]*/gi)?.length ?? 0; 167 | bodyLineCount += 1; // Extra line break at the end 168 | 169 | let messageLines: string = ''; 170 | 171 | messageLines += MessageKind[message.kind] + '\n'; 172 | messageLines += message.id + '\n'; 173 | messageLines += MessageStatus[message.content.status] + '\n'; 174 | messageLines += bodyLineCount + '\n'; 175 | messageLines += message.content.body + '\n'; 176 | 177 | return await this.socket.writeLine(messageLines); 178 | } 179 | 180 | async sendRequest(id: string, body: string): Promise { 181 | let responseListener: ResponseListener; 182 | 183 | let msg = new Message(MessageKind.Request, id, new MessageContent(MessageStatus.Ok, body)); 184 | let written = await this.writeMessage(msg) > 0; 185 | 186 | if (!written) { 187 | return undefined; 188 | } 189 | 190 | return new Promise((resolve) => { 191 | let queue = this.requestAwaiterQueues.get(id); 192 | 193 | if (queue === undefined) { 194 | queue = []; 195 | this.requestAwaiterQueues.set(id, queue); 196 | } 197 | 198 | responseListener = (response) => { 199 | resolve(response); 200 | }; 201 | queue.push(responseListener); 202 | }); 203 | } 204 | } 205 | 206 | export class Client implements Disposable { 207 | identity: string; 208 | projectDir: string; 209 | projectMetadataDir: string; 210 | metaFilePath: string; 211 | messageHandler: IMessageHandler; 212 | logger: ILogger; 213 | 214 | fsWatcher?: chokidar.FSWatcher; 215 | metaFileModifiedTime?: Date; 216 | 217 | metadata?: GodotIdeMetadata; 218 | isDisposed: boolean = false; 219 | 220 | peer?: Peer; 221 | 222 | constructor(identity: string, godotProjectDir: string, messageHandler: IMessageHandler, logger: ILogger) { 223 | this.identity = identity; 224 | this.messageHandler = messageHandler; 225 | this.logger = logger; 226 | 227 | this.projectDir = godotProjectDir; 228 | this.projectMetadataDir = path.join(godotProjectDir, '.mono', 'metadata'); 229 | 230 | this.metaFilePath = path.join(this.projectMetadataDir, GodotIdeMetadata.defaultFileName); 231 | } 232 | 233 | getGodotProjectDir(): string { 234 | return this.projectDir; 235 | } 236 | 237 | isConnected(): boolean { 238 | return !this.isDisposed && this.peer !== undefined && this.peer.isConnected; 239 | } 240 | 241 | dispose() { 242 | this.isDisposed = true; 243 | this.fsWatcher?.close(); 244 | this.peer?.dispose(); 245 | } 246 | 247 | start(): void { 248 | this.startWatching(); 249 | 250 | if (this.isDisposed || this.isConnected()) { 251 | return; 252 | } 253 | 254 | if (!fs.existsSync(this.metaFilePath)) { 255 | this.logger.logInfo('There is no Godot Ide Server running'); 256 | return; 257 | } 258 | 259 | // Check to store the modified time. Needed for the onMetaFileChanged check to work. 260 | if (!this.metaFileModifiedTimeChanged()) { 261 | return; 262 | } 263 | 264 | const metadata = this.readMetadataFile(); 265 | 266 | if (metadata !== undefined && metadata !== this.metadata) { 267 | this.metadata = metadata; 268 | this.connectToServer(); 269 | } 270 | } 271 | 272 | async connectToServer(): Promise { 273 | if (this.peer !== undefined && this.peer.isConnected) { 274 | this.logger.logError('Attempted to connect to Godot Ide Server again when already connected'); 275 | return; 276 | } 277 | 278 | const attempts = 3; 279 | let attemptsLeft = attempts; 280 | 281 | while (attemptsLeft-- > 0) { 282 | if (attemptsLeft < (attempts - 1)) { 283 | this.logger.logInfo(`Waiting 3 seconds... (${attemptsLeft + 1} attempts left)`); 284 | await timeout(5000); 285 | } 286 | 287 | const socket = new CustomSocket(); 288 | 289 | this.logger.logInfo('Connecting to Godot Ide Server'); 290 | 291 | try { 292 | await socket.connect(this.metadata!.port, 'localhost'); 293 | } 294 | catch (err) { 295 | this.logger.logError('Failed to connect to Godot Ide Server', err as Error); 296 | continue; 297 | } 298 | 299 | this.logger.logInfo('Connection open with Godot Ide Server'); 300 | 301 | this.peer?.dispose(); 302 | this.peer = new Peer(socket, new ClientHandshake(), this.messageHandler, this.logger); 303 | 304 | if (!await this.peer.doHandshake(this.identity)) { 305 | this.logger.logError('Handshake failed'); 306 | this.peer.dispose(); 307 | continue; 308 | } 309 | 310 | await this.peer.process(); 311 | 312 | this.logger.logInfo('Connection closed with Ide Client'); 313 | 314 | return; 315 | } 316 | 317 | this.logger.logInfo(`Failed to connect to Godot Ide Server after ${attempts} attempts`); 318 | } 319 | 320 | startWatching(): void { 321 | this.fsWatcher = chokidar.watch(this.metaFilePath); 322 | this.fsWatcher.on('add', path => this.onMetaFileChanged()); 323 | this.fsWatcher.on('change', path => this.onMetaFileChanged()); 324 | this.fsWatcher.on('unlink', path => this.onMetaFileDeleted()); 325 | } 326 | 327 | metaFileModifiedTimeChanged(): boolean { 328 | const stats = fs.statSync(this.metaFilePath); 329 | if (this.metaFileModifiedTime !== undefined && 330 | stats.mtime.valueOf() === this.metaFileModifiedTime.valueOf()) { 331 | return false; 332 | } 333 | this.metaFileModifiedTime = stats.mtime; 334 | return true; 335 | } 336 | 337 | onMetaFileChanged(): void { 338 | if (this.isDisposed) { 339 | return; 340 | } 341 | 342 | if (!fs.existsSync(this.metaFilePath)) { 343 | return; 344 | } 345 | 346 | // Check the modified time to discard some irrelevant changes 347 | if (!this.metaFileModifiedTimeChanged()) { 348 | return; 349 | } 350 | 351 | const metadata = this.readMetadataFile(); 352 | 353 | if (metadata !== undefined && metadata !== this.metadata) { 354 | this.metadata = metadata; 355 | this.connectToServer(); 356 | } 357 | } 358 | 359 | onMetaFileDeleted(): void { 360 | if (this.isConnected() || !fs.existsSync(this.metaFilePath)) { 361 | return; 362 | } 363 | 364 | const metadata = this.readMetadataFile(); 365 | 366 | if (metadata !== undefined) { 367 | this.metadata = metadata; 368 | this.connectToServer(); 369 | } 370 | } 371 | 372 | readMetadataFile(): GodotIdeMetadata | undefined { 373 | const buffer = fs.readFileSync(this.metaFilePath); 374 | const metaFileContent = buffer.toString('utf-8'); 375 | const lines = metaFileContent.replace('\r\n', '\n').split('\n'); 376 | 377 | if (lines.length < 2) { 378 | return undefined; 379 | } 380 | 381 | const port: number = parseInt(lines[0]); 382 | const editorExecutablePath = lines[1]; 383 | 384 | if (isNaN(port)) { 385 | return undefined; 386 | } 387 | 388 | return new GodotIdeMetadata(port, editorExecutablePath); 389 | } 390 | } 391 | 392 | class GodotIdeMetadata { 393 | port: number; 394 | editorExecutablePath: string; 395 | 396 | static readonly defaultFileName = 'ide_messaging_meta.txt'; 397 | 398 | constructor(port: number, editorExecutablePath: string) { 399 | this.port = port; 400 | this.editorExecutablePath = editorExecutablePath; 401 | } 402 | 403 | equals(other: GodotIdeMetadata): boolean { 404 | return this.port === other.port && this.editorExecutablePath === other.editorExecutablePath; 405 | } 406 | } 407 | 408 | function escapeRegex(s: string): string { 409 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 410 | } 411 | 412 | class ClientHandshake implements IHandshake { 413 | readonly clientHandshakeBase: string = `${Peer.clientHandshakeName},Version=${Peer.protocolVersionMajor}.${Peer.protocolVersionMinor}.${Peer.protocolVersionRevision}`; 414 | readonly serverHandshakePattern: RegExp = new RegExp(escapeRegex(Peer.serverHandshakeName) + /,Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{0,63})/.source); 415 | 416 | getHandshakeLine(identity: string): string { 417 | return `${this.clientHandshakeBase},${identity}`; 418 | } 419 | 420 | isValidPeerHandshake(handshake: string, logger: ILogger): [boolean, string | undefined] { 421 | let match = this.serverHandshakePattern.exec(handshake); 422 | 423 | if (match === null) { 424 | return [false, undefined]; 425 | } 426 | 427 | let serverMajor = parseInt(match[1], 10); 428 | if (isNaN(serverMajor) || Peer.protocolVersionMajor !== serverMajor) { 429 | logger.logDebug('Incompatible major version: ' + match[1]); 430 | return [false, undefined]; 431 | } 432 | 433 | let serverMinor = parseInt(match[2], 10); 434 | if (isNaN(serverMinor) || Peer.protocolVersionMinor < serverMinor) { 435 | logger.logDebug('Incompatible minor version: ' + match[2]); 436 | return [false, undefined]; 437 | } 438 | 439 | let serverRevision = parseInt(match[3], 10); 440 | if (isNaN(serverRevision)) { 441 | logger.logDebug('Incompatible revision build: ' + match[3]); 442 | return [false, undefined]; 443 | } 444 | 445 | let identity = match[4]; 446 | 447 | return [true, identity]; 448 | } 449 | } 450 | 451 | interface IHandshake { 452 | getHandshakeLine(identity: string): string; 453 | isValidPeerHandshake(handshake: string, logger: ILogger): [boolean, string | undefined]; 454 | } 455 | 456 | export interface IMessageHandler { 457 | handleRequest(peer: Peer, id: string, content: MessageContent, logger: ILogger): Promise; 458 | } 459 | 460 | export interface ILogger { 461 | logDebug(message: string): void; 462 | logInfo(message: string): void; 463 | logWarning(message: string): void; 464 | logError(message: string): void; 465 | logError(message: string, e: Error): void; 466 | } 467 | 468 | class Message { 469 | kind: MessageKind; 470 | id: string; 471 | content: MessageContent; 472 | 473 | constructor(kind: MessageKind, id: string, content: MessageContent) { 474 | this.kind = kind; 475 | this.id = id; 476 | this.content = content; 477 | } 478 | 479 | toString(): string { 480 | return `${this.kind} | ${this.id}`; 481 | } 482 | } 483 | 484 | enum MessageKind { 485 | Request, 486 | Response 487 | } 488 | 489 | export enum MessageStatus { 490 | Ok, 491 | RequestNotSupported, 492 | InvalidRequestBody 493 | } 494 | 495 | export class MessageContent { 496 | status: MessageStatus; 497 | body: string; 498 | 499 | constructor(status: MessageStatus, body: string) { 500 | this.status = status; 501 | this.body = body; 502 | } 503 | } 504 | 505 | class DecodedMessage { 506 | kind?: MessageKind; 507 | id?: string; 508 | status?: MessageStatus; 509 | body: string = ''; 510 | pendingBodyLines?: number; 511 | 512 | clear(): void { 513 | this.kind = undefined; 514 | this.id = undefined; 515 | this.status = undefined; 516 | this.body = ''; 517 | this.pendingBodyLines = undefined; 518 | } 519 | 520 | toMessage(): Message | undefined { 521 | if (this.kind === undefined || this.id === undefined || this.status === undefined || 522 | this.pendingBodyLines === undefined || this.pendingBodyLines > 0) { 523 | return undefined; 524 | } 525 | 526 | return new Message(this.kind, this.id, new MessageContent(this.status, this.body)); 527 | } 528 | } 529 | 530 | enum MessageDecoderState { 531 | Decoding, 532 | Decoded, 533 | Errored 534 | } 535 | 536 | function tryParseEnumCaseInsensitive(enumObj: T, value: string): T[keyof T] | undefined { 537 | let key = Object.keys(enumObj).find(key => key.toLowerCase() === value.toLowerCase()); 538 | if (key === undefined) { 539 | return undefined; 540 | } 541 | return enumObj[key]; 542 | } 543 | 544 | class MessageDecoder { 545 | readonly decodingMessage: DecodedMessage = new DecodedMessage(); 546 | 547 | decode(messageLine: string): [MessageDecoderState, Message | undefined] { 548 | if (this.decodingMessage.kind === undefined) { 549 | let kind = tryParseEnumCaseInsensitive(MessageKind, messageLine); 550 | if (kind === undefined) { 551 | this.decodingMessage.clear(); 552 | return [MessageDecoderState.Errored, undefined]; 553 | } 554 | this.decodingMessage.kind = kind; 555 | } else if (this.decodingMessage.id === undefined) { 556 | this.decodingMessage.id = messageLine; 557 | } else if (this.decodingMessage.status === undefined) { 558 | let status = tryParseEnumCaseInsensitive(MessageStatus, messageLine); 559 | if (status === undefined) { 560 | this.decodingMessage.clear(); 561 | return [MessageDecoderState.Errored, undefined]; 562 | } 563 | this.decodingMessage.status = status; 564 | } else if (this.decodingMessage.pendingBodyLines === undefined) { 565 | let pendingBodyLines = parseInt(messageLine); 566 | if (isNaN(pendingBodyLines)) { 567 | this.decodingMessage.clear(); 568 | return [MessageDecoderState.Errored, undefined]; 569 | } 570 | this.decodingMessage.pendingBodyLines = pendingBodyLines; 571 | } else { 572 | if (this.decodingMessage.pendingBodyLines > 0) { 573 | this.decodingMessage.body += messageLine + '\n'; 574 | this.decodingMessage.pendingBodyLines -= 1; 575 | } else { 576 | let decodedMessage = this.decodingMessage.toMessage(); 577 | this.decodingMessage.clear(); 578 | return [MessageDecoderState.Decoded, decodedMessage]; 579 | } 580 | } 581 | 582 | return [MessageDecoderState.Decoding, undefined]; 583 | } 584 | } 585 | --------------------------------------------------------------------------------