├── images └── icon.png ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── screenshots ├── screenshot-bundle.png ├── screenshot-defold.png ├── screenshot-deploy.png ├── screenshot-suggest.png ├── screenshot-tasks.png ├── screenshot-commands.png ├── screenshot-debugger.png ├── screenshot-platforms.png ├── screenshot-settings.png ├── screenshot-annotations.png ├── screenshot-extensions.png └── screenshot-preferences.png ├── .gitignore ├── .vscodeignore ├── tsconfig.json ├── resources └── debugger │ ├── debugger.script │ └── debugger.lua ├── .eslintrc.json ├── src ├── logger.ts ├── data │ ├── debuggers.ts │ ├── extensions.ts │ └── settings.ts ├── tasks.ts ├── shell.ts ├── deployer.ts ├── momento.ts ├── utils.ts ├── migration.ts ├── extension.ts ├── bob.ts ├── launcher.ts ├── commands.ts ├── config.ts ├── annotations.ts └── wizard.ts ├── LICENSE ├── snippets └── defold.json ├── CHANGELOG.md ├── package.json └── README.md /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/images/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /screenshots/screenshot-bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-bundle.png -------------------------------------------------------------------------------- /screenshots/screenshot-defold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-defold.png -------------------------------------------------------------------------------- /screenshots/screenshot-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-deploy.png -------------------------------------------------------------------------------- /screenshots/screenshot-suggest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-suggest.png -------------------------------------------------------------------------------- /screenshots/screenshot-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-tasks.png -------------------------------------------------------------------------------- /screenshots/screenshot-commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-commands.png -------------------------------------------------------------------------------- /screenshots/screenshot-debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-debugger.png -------------------------------------------------------------------------------- /screenshots/screenshot-platforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-platforms.png -------------------------------------------------------------------------------- /screenshots/screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-settings.png -------------------------------------------------------------------------------- /screenshots/screenshot-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-annotations.png -------------------------------------------------------------------------------- /screenshots/screenshot-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-extensions.png -------------------------------------------------------------------------------- /screenshots/screenshot-preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrochili/vscode-defold/HEAD/screenshots/screenshot-preferences.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # Development 6 | out 7 | dist 8 | node_modules 9 | .vscode-test/ 10 | *.vsix -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false 4 | }, 5 | "search.exclude": { 6 | "out": true 7 | }, 8 | "typescript.tsc.autoDetect": "off" 9 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | */** 4 | 5 | # Whitelist 6 | !package.json 7 | !README.md 8 | !CHANGELOG.md 9 | !LICENSE 10 | !images 11 | !snippets 12 | !resources 13 | !out/extension.js -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020", 8 | "ES2021.String" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "esbuild", 7 | "isBackground": true, 8 | "problemMatcher": "$tsc-watch", 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/out/**/*.js" 13 | ], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /resources/debugger/debugger.script: -------------------------------------------------------------------------------- 1 | --[[ 2 | debugger.script 3 | github.com/astrochili/vscode-defold 4 | Copyright (c) 2023 Roman Silin 5 | MIT license. See LICENSE for details. 6 | 7 | Helpful script to debug the Defold project with Local Lua Debugger in VSCode. 8 | Add it to your initial collection. 9 | --]] 10 | 11 | local debugger = require('debugger.debugger') 12 | debugger.start() -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | 14 | let outputChannel: vscode.LogOutputChannel 15 | 16 | interface LogOptions { 17 | openOutput: boolean 18 | } 19 | 20 | export default function log(message: string, options?: LogOptions) { 21 | if (!outputChannel) { 22 | outputChannel = vscode.window.createOutputChannel( 23 | config.extension.displayName, 24 | { log: true } 25 | ) 26 | } 27 | 28 | outputChannel.appendLine(message) 29 | 30 | if (options?.openOutput) { 31 | outputChannel.show() 32 | } 33 | 34 | console.log(message) 35 | } -------------------------------------------------------------------------------- /src/data/debuggers.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as extensions from './extensions' 12 | import * as tasks from '../tasks' 13 | 14 | export const recommended = { 15 | [extensions.ids.localLuaDebugger]: { 16 | 'name': 'Defold Kit', 17 | 'type': 'lua-local', 18 | 'request': 'launch', 19 | 'stopOnEntry': false, 20 | 'verbose': false, 21 | 'internalConsoleOptions': 'openOnSessionStart', 22 | 'program': { 'command': 'build/defoldkit/dmengine' }, 23 | 'args': ['build/defoldkit/game.projectc'], 24 | 'windows': { 25 | 'program': { 'command': 'build\\defoldkit\\dmengine.exe' }, 26 | 'args': ['build\\defoldkit\\game.projectc'] 27 | }, 28 | 'preLaunchTask': `Defold Kit: ${tasks.buildTaskName}` 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Roman Silin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/tasks.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | 14 | function makeTask(name: string, command: string, group: vscode.TaskGroup | undefined): vscode.Task { 15 | const task = new vscode.Task( 16 | { type: 'shell' }, 17 | vscode.TaskScope.Workspace, 18 | name, 19 | 'Defold Kit', 20 | new vscode.ShellExecution('exit${command:' + config.extension.commandPrefix + '.' + command + '}') 21 | ) 22 | 23 | task.group = group 24 | task.presentationOptions = { 25 | echo: false, 26 | focus: false, 27 | reveal: vscode.TaskRevealKind.Silent, 28 | showReuseMessage: false, 29 | close: true, 30 | clear: true 31 | } 32 | 33 | return task 34 | } 35 | 36 | export const buildTaskName = 'Build to Launch' 37 | 38 | export function makeTasks(): vscode.Task[] { 39 | return [ 40 | makeTask(buildTaskName, 'build', undefined), 41 | makeTask('Bundle', 'bundle', vscode.TaskGroup.Build), 42 | makeTask('Clean Build', 'cleanBuild', vscode.TaskGroup.Build), 43 | makeTask('Resolve Dependencies', 'resolve', vscode.TaskGroup.Build), 44 | makeTask('Deploy to Mobile', 'deploy', vscode.TaskGroup.Build) 45 | ] 46 | } -------------------------------------------------------------------------------- /src/data/extensions.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | export const ids = { 12 | luaLanguageServer: 'sumneko.lua', 13 | localLuaDebugger: 'tomblind.local-lua-debugger-vscode', 14 | textProtoId: 'thejustinwalsh.textproto-grammer', 15 | glslLanguage: 'slevesque.shader', 16 | glslLint: 'dtoplak.vscode-glsllint' 17 | } 18 | 19 | export const recommended = { 20 | [ids.luaLanguageServer]: { 21 | title: 'Lua Language Server', 22 | detail: '[Required] Autocompletion, annotations, diagnostics and etc.', 23 | picked: true 24 | }, 25 | [ids.localLuaDebugger]: { 26 | title: 'Local Lua Debugger', 27 | detail: '[Recommended] Launching the game to debug with breakpoints', 28 | picked: true 29 | }, 30 | [ids.textProtoId]: { 31 | title: 'Text Proto', 32 | detail: '[Optional] Syntax highlighting for `.collection`, `.go` and other Protobuf files', 33 | picked: false 34 | }, 35 | [ids.glslLanguage]: { 36 | title: 'Shader Languages Support for Visual Studio Code', 37 | detail: '[Optional] GLSL support for `.vp` and `.fp` files', 38 | picked: false 39 | }, 40 | [ids.glslLint]: { 41 | title: 'GLSL Linting for Visual Studio Code', 42 | detail: '[Optional] GLSL linting for `.vp` and `.fp` files', 43 | picked: false 44 | } 45 | } -------------------------------------------------------------------------------- /resources/debugger/debugger.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | debugger.lua 3 | github.com/astrochili/vscode-defold 4 | Copyright (c) 2023 Roman Silin 5 | MIT license. See LICENSE for details. 6 | 7 | Helpful module to debug the Defold project with Local Lua Debugger in VSCode. 8 | Require it to your initial script or module and start the debugger session. 9 | See an example in the `debugger.script` file. 10 | --]] 11 | 12 | local dummy = { 13 | error = '' 14 | } 15 | 16 | dummy.start = function() 17 | print('VSCode debugger hasn\'t started. ' .. dummy.error) 18 | end 19 | 20 | dummy.requestBreak = function() 21 | print('VSCode debugger hasn\'t started. ' .. dummy.error) 22 | end 23 | 24 | if os.getenv('LOCAL_LUA_DEBUGGER_VSCODE') ~= '1' then 25 | dummy.error = 'Not the VSCode environment or the Local Lua Debugger extension is not installed.' 26 | return dummy 27 | end 28 | 29 | local debugger_path 30 | 31 | for path in package.path:gmatch('([^;]+)') do 32 | if path:find('local%-lua%-debugger') then 33 | debugger_path = path:gsub('?.lua', 'lldebugger.lua') 34 | end 35 | end 36 | 37 | if not debugger_path then 38 | dummy.error = 'Path `local-lua-debugger doesn\'t` exist in package.path.' 39 | return dummy 40 | end 41 | 42 | local debugger_file = io.open(debugger_path, 'r') 43 | local is_file_exists = debugger_file ~= nil 44 | 45 | if is_file_exists then 46 | io.close(debugger_file) 47 | else 48 | dummy.error = 'File doesn\'t exist: ' .. debugger_path 49 | return dummy 50 | end 51 | 52 | local debugger = { } 53 | local module, error = loadfile(debugger_path) 54 | 55 | if module then 56 | package.loaded['lldebugger'] = module() 57 | 58 | local import = require 59 | debugger = import 'lldebugger' 60 | 61 | return debugger 62 | elseif error then 63 | dummy.error = 'Loading file error, ' .. error 64 | 65 | return dummy 66 | end -------------------------------------------------------------------------------- /src/shell.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as childProcess from 'child_process' 12 | import * as config from './config' 13 | import log from './logger' 14 | 15 | interface ShellResult { 16 | success: boolean, 17 | output: string 18 | } 19 | 20 | export async function execute( 21 | label: string, 22 | executable: string, 23 | args: string[] = [], 24 | cwd?: string 25 | ): Promise { 26 | const command = `${executable} ${args.join(' ')}` 27 | log(`${label} $: ${command}`) 28 | 29 | return new Promise((resolve) => { 30 | let output = '' 31 | 32 | log(`${label}: [starting]`) 33 | const child = childProcess.exec(command, { 34 | cwd: cwd ?? config.paths.workspace 35 | }) 36 | 37 | child.stdout?.on('data', data => { 38 | const message = data.trim() as string 39 | 40 | if (message) { 41 | output += `${message}\n` 42 | log(`${label}: [stdout] ${message}`) 43 | } 44 | }) 45 | 46 | child.stderr?.on('data', data => { 47 | log(`${label}: [stderr] ${data.trim()}`) 48 | }) 49 | 50 | child.on('error', error => { 51 | log(`${label}: [error] ${error.message}`, { openOutput: true }) 52 | 53 | return resolve({ 54 | success: false, 55 | output: output 56 | }) 57 | }) 58 | 59 | child.on('close', code => { 60 | const success = code == 0 61 | log(`${label}: [exit ${code}]`, { openOutput: !success }) 62 | 63 | return resolve({ 64 | success: success, 65 | output: output 66 | }) 67 | }) 68 | }) 69 | } -------------------------------------------------------------------------------- /src/deployer.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as shell from './shell' 13 | import log from './logger' 14 | import path = require('path') 15 | 16 | export async function deploy(target: string): Promise { 17 | const bundlePath = path.join('bundle', target) 18 | 19 | let bundleExtension: string 20 | let deployExecutable: string 21 | 22 | switch (target) { 23 | case 'iOS': 24 | bundleExtension = 'ipa' 25 | deployExecutable = 'ios-deploy -b' 26 | break 27 | 28 | case 'Android': 29 | bundleExtension = 'apk' 30 | deployExecutable = 'adb install' 31 | break 32 | 33 | default: 34 | vscode.window.showWarningMessage(`Unknown target OS for deploying: ${target}`) 35 | log(`Unknown target OS for deploying: ${target}`) 36 | return false 37 | } 38 | 39 | const searchPattern = `${bundlePath}${path.sep}**${path.sep}*.${bundleExtension}` 40 | let files: vscode.Uri[] 41 | 42 | try { 43 | files = await vscode.workspace.findFiles(searchPattern) 44 | } catch (error) { 45 | vscode.window.showErrorMessage(`Failed to deploy. See Output for details.`) 46 | log(`Failed to find the bundle with pattern: ${searchPattern}`) 47 | log(`${error}`) 48 | return false 49 | } 50 | 51 | if (files.length == 0) { 52 | vscode.window.showWarningMessage(`Failed to deploy, prepare the bundle first. The '${searchPattern}' file not found.`) 53 | log(`Failed to find the bundle with pattern: ${searchPattern}`) 54 | return false 55 | } 56 | 57 | const bundleFile = files[0].fsPath 58 | 59 | const result = await shell.execute('Deploy', deployExecutable, [`"${bundleFile}"`]) 60 | return result.success 61 | } -------------------------------------------------------------------------------- /src/momento.ts: -------------------------------------------------------------------------------- 1 | import * as config from './config' 2 | import * as vscode from 'vscode' 3 | 4 | export const keys = { 5 | dontSuggestSetup: 'dontSuggestSetup', 6 | bundleTarget: 'bundleTarget', 7 | bundleOption: 'bundleOption', 8 | extensionInstallation: 'extensionInstallation', 9 | settingsApplying: 'settingsApplying', 10 | lastMigrationVersion: 'lastMigrationVersion', 11 | annotationsVersion: 'annotationsVersion', 12 | onceSetup: 'onceSetup', 13 | libsFolderHash: 'libsFolderHash' 14 | } 15 | 16 | export async function savePickerSelection( 17 | items: vscode.QuickPickItem[], 18 | picked: vscode.QuickPickItem[], 19 | memento: vscode.Memento, 20 | momentoKey: string, 21 | ) { 22 | for (const item of items) { 23 | await memento.update(`${momentoKey}:${item.label}`, picked.includes(item)) 24 | } 25 | } 26 | 27 | export function loadPickerSelection(items: vscode.QuickPickItem[], memento: vscode.Memento, momentoKey: string) { 28 | for (const item of items) { 29 | const picked = memento.get(`${momentoKey}:${item.label}`) as boolean 30 | item.picked = picked == undefined ? item.picked : picked 31 | } 32 | } 33 | 34 | export function getLastGlobalMigrationVersion(): string { 35 | return config.context.globalState.get(keys.lastMigrationVersion) as string ?? config.lastVersionWithoutMigrationTracking 36 | } 37 | 38 | export async function setLastGlobalMigrationVersion(version: string) { 39 | await config.context.globalState.update(keys.lastMigrationVersion, version) 40 | } 41 | 42 | export function getLastWorkspaceMigrationVersion(): string { 43 | return config.context.workspaceState.get(keys.lastMigrationVersion) as string ?? config.lastVersionWithoutMigrationTracking 44 | } 45 | 46 | export async function setLastWorkspaceMigrationVersion(version: string) { 47 | await config.context.workspaceState.update(keys.lastMigrationVersion, version) 48 | } 49 | 50 | export function getAnnotationsVersion(): string|undefined { 51 | return config.context.globalState.get(keys.annotationsVersion) as string 52 | } 53 | 54 | export async function setAnnotationsVersion(version: string) { 55 | await config.context.globalState.update(keys.annotationsVersion, version) 56 | } 57 | 58 | export function getOnceSetup(): boolean { 59 | return config.context.workspaceState.get(keys.onceSetup) as boolean ?? false 60 | } 61 | 62 | export async function setOnceSetup(value: boolean) { 63 | await config.context.workspaceState.update(keys.onceSetup, value) 64 | } 65 | 66 | export function getLibsFolderHash(): number|undefined { 67 | return config.context.globalState.get(keys.libsFolderHash) as number 68 | } 69 | 70 | export async function setLibsFolderHash(hash: number) { 71 | await config.context.globalState.update(keys.libsFolderHash, hash) 72 | } 73 | -------------------------------------------------------------------------------- /src/data/settings.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as config from '../config' 12 | import { ids } from './extensions' 13 | 14 | export const recommended = { 15 | [config.extension.id]: { 16 | 'files.associations': { 17 | '*.project': 'ini', 18 | '*.script': 'lua', 19 | '*.gui_script': 'lua', 20 | '*.render_script': 'lua', 21 | '*.editor_script': 'lua', 22 | '*.fp': 'glsl', 23 | '*.vp': 'glsl', 24 | '*.go': 'textproto', 25 | '*.animationset': 'textproto', 26 | '*.atlas': 'textproto', 27 | '*.buffer': 'json', 28 | '*.camera': 'textproto', 29 | '*.collection': 'textproto', 30 | '*.collectionfactory': 'textproto', 31 | '*.collectionproxy': 'textproto', 32 | '*.collisionobject': 'textproto', 33 | '*.display_profiles': 'textproto', 34 | '*.factory': 'textproto', 35 | '*.gamepads': 'textproto', 36 | '*.gui': 'textproto', 37 | '*.input_binding': 'textproto', 38 | '*.label': 'textproto', 39 | '*.material': 'textproto', 40 | '*.mesh': 'textproto', 41 | '*.model': 'textproto', 42 | '*.particlefx': 'textproto', 43 | '*.render': 'textproto', 44 | '*.sound': 'textproto', 45 | '*.spinemodel': 'textproto', 46 | '*.spinescene': 'textproto', 47 | '*.sprite': 'textproto', 48 | '*.texture_profiles': 'textproto', 49 | '*.tilemap': 'textproto', 50 | '*.tilesource': 'textproto', 51 | '*.manifest': 'textproto', 52 | '*.appmanifest': 'yaml', 53 | '*.script_api': 'yaml', 54 | 'ext.manifest': 'yaml' 55 | } 56 | }, 57 | 58 | [ids.luaLanguageServer]: { 59 | '[lua]': { 60 | 'editor.defaultFormatter': ids.luaLanguageServer 61 | }, 62 | 'Lua.runtime.version': 'Lua 5.1', 63 | 'Lua.runtime.pathStrict': true, 64 | 'Lua.window.statusBar': false, 65 | 'Lua.completion.callSnippet': 'Replace', 66 | 'Lua.completion.keywordSnippet': 'Replace', 67 | 'Lua.completion.showWord': 'Fallback', 68 | 'Lua.completion.autoRequire': false, 69 | 'Lua.diagnostics.libraryFiles': 'Disable', 70 | 'Lua.diagnostics.disable': [ 71 | 'lowercase-global', 72 | 'redefined-local' 73 | ], 74 | 'Lua.workspace.library': [ 75 | ] 76 | }, 77 | 78 | [ids.glslLint]: { 79 | 'glsllint.additionalStageAssociations': { 80 | '.fp': 'frag', 81 | '.vp': 'vert' 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as shell from './shell' 13 | import log from './logger' 14 | 15 | const encoder = new TextEncoder() 16 | 17 | export const isMac = process.platform == 'darwin' 18 | export const isWindows = process.platform == 'win32' 19 | export const isLinux = process.platform == 'linux' 20 | 21 | export function compareVersions(a: string, b: string): number { 22 | return a.localeCompare(b, undefined, { 23 | numeric: true, 24 | sensitivity: 'base' 25 | }) 26 | } 27 | 28 | export function hash(text: string): number { 29 | let hash = 0 30 | 31 | if (text.length === 0) { 32 | return hash 33 | } 34 | 35 | for (let index = 0; index < text.length; index++) { 36 | const char = text.charCodeAt(index); 37 | hash = ((hash << 5) - hash) + char; 38 | hash |= 0 39 | } 40 | 41 | return hash 42 | } 43 | 44 | 45 | export function settingsString(key: string): string | undefined { 46 | return vscode.workspace.getConfiguration().get(key) 47 | } 48 | 49 | export function settingsBoolean(key: string): boolean | undefined { 50 | return vscode.workspace.getConfiguration().get(key) 51 | } 52 | 53 | export async function isProcessRunning(process: string): Promise { 54 | const command = isWindows ? `tasklist` : `pgrep` 55 | const args = isWindows ? [] : ['-l', process, `|`, `awk`, `'{ print $2 }'`] 56 | 57 | const result = await shell.execute('Process', command, args) 58 | 59 | if (!result) { 60 | return false 61 | } 62 | 63 | const isRunning = result.output.toLowerCase().indexOf(process.toLowerCase()) > -1 64 | return isRunning 65 | } 66 | 67 | export async function isPathExists(path: string): Promise { 68 | const uri = vscode.Uri.file(path) 69 | 70 | try { 71 | await vscode.workspace.fs.stat(uri) 72 | return true 73 | } catch (error) { 74 | return false 75 | } 76 | } 77 | 78 | export async function readDirectory(path: string): Promise<[string, vscode.FileType][] | undefined> { 79 | try { 80 | const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path)) 81 | return files 82 | } catch (error) { 83 | log(`Exception occured during reading directory: ${error}`) 84 | return undefined 85 | } 86 | } 87 | 88 | export async function createDirectory(path: string): Promise { 89 | try { 90 | await vscode.workspace.fs.createDirectory(vscode.Uri.file(path)) 91 | return true 92 | } catch (error) { 93 | log(`Exception occured during creating directory: ${error}`) 94 | return false 95 | } 96 | } 97 | 98 | export async function readTextFile(path: string): Promise { 99 | const data = await readDataFile(path) 100 | 101 | if (data) { 102 | return data.toString() 103 | } 104 | } 105 | 106 | export async function readDataFile(path: string): Promise { 107 | try { 108 | const data = await vscode.workspace.fs.readFile(vscode.Uri.file(path)) 109 | return data 110 | } catch (error) { 111 | log(`Exception occured during reading file: ${error}`) 112 | return undefined 113 | } 114 | } 115 | 116 | export async function writeTextFile(path: string, text: string): Promise { 117 | try { 118 | const data = encoder.encode(text) 119 | return writeDataFile(path, data) 120 | } catch (error) { 121 | log(`Exception occured during encoding text: ${error}`) 122 | return false 123 | } 124 | } 125 | 126 | export async function writeDataFile(path: string, data: Uint8Array): Promise { 127 | try { 128 | await vscode.workspace.fs.writeFile(vscode.Uri.file(path), data) 129 | return true 130 | } catch (error) { 131 | log(`Exception occured during writing file: ${error}`) 132 | return false 133 | } 134 | } 135 | 136 | export async function deleteFile(path: string): Promise { 137 | try { 138 | await vscode.workspace.fs.delete(vscode.Uri.file(path), { recursive: true, useTrash: false }) 139 | return true 140 | } catch (error) { 141 | log(`Exception occured during deleting file: ${error}`) 142 | return false 143 | } 144 | } 145 | 146 | export async function copy(path: string, target: string): Promise { 147 | try { 148 | await vscode.workspace.fs.copy(vscode.Uri.file(path), vscode.Uri.file(target), { overwrite: true }) 149 | return true 150 | } catch (error) { 151 | log(`Exception occured during copying file: ${error}`) 152 | return false 153 | } 154 | } -------------------------------------------------------------------------------- /snippets/defold.json: -------------------------------------------------------------------------------- 1 | { 2 | "defold": [ 3 | { 4 | "isFileTemplate": true, 5 | "scope": "lua", 6 | "prefix": "script", 7 | "body": [ 8 | "---@class ${1:${TM_FILENAME}}", 9 | "", 10 | "---@package", 11 | "---@param self ${1:${TM_FILENAME}}", 12 | "function init(self)", 13 | "\t", 14 | "end", 15 | "", 16 | "---@package", 17 | "---@param self ${1:${TM_FILENAME}}", 18 | "---@param dt number", 19 | "function update(self, dt)", 20 | "\t", 21 | "end", 22 | "", 23 | "---@package", 24 | "---@param self ${1:${TM_FILENAME}}", 25 | "---@param message_id hash", 26 | "---@param message table", 27 | "---@param sender url", 28 | "function on_message(self, message_id, message, sender)", 29 | "\t", 30 | "end", 31 | "", 32 | "---@package", 33 | "---@param self ${1:${TM_FILENAME}}", 34 | "---@param action_id hash", 35 | "---@param action on_input.action", 36 | "---@return boolean|nil", 37 | "function on_input(self, action_id, action)", 38 | "\t", 39 | "end", 40 | "", 41 | "---@package", 42 | "---@param self ${1:${TM_FILENAME}}", 43 | "function final(self)", 44 | "\t", 45 | "end" 46 | ], 47 | "description": "Defold script file with annotated lifecycle methods" 48 | }, 49 | { 50 | "body": [ 51 | "---@package", 52 | "---@param self ${1:${TM_FILENAME}}", 53 | "function init(self)", 54 | "\t${2}", 55 | "end" 56 | ], 57 | "prefix": "init(self)", 58 | "description": "Defold script init() function" 59 | }, 60 | { 61 | "body": [ 62 | "---@package", 63 | "---@param self ${1:${TM_FILENAME}}", 64 | "---@param dt number", 65 | "function update(self, dt)", 66 | "\t${2}", 67 | "end" 68 | ], 69 | "prefix": "update(self, dt)", 70 | "description": "Defold script update() function" 71 | }, 72 | { 73 | "body": [ 74 | "---@package", 75 | "---@param self ${1:${TM_FILENAME}}", 76 | "---@param dt number", 77 | "function fixed_update(self, dt)", 78 | "\t${2}", 79 | "end" 80 | ], 81 | "prefix": "fixed_update(self, dt)", 82 | "description": "Defold script fixed_update() function" 83 | }, 84 | { 85 | "body": [ 86 | "---@package", 87 | "---@param self ${1:${TM_FILENAME}}", 88 | "---@param message_id hash", 89 | "---@param message table", 90 | "---@param sender url", 91 | "function on_message(self, message_id, message, sender)", 92 | "\t${2}", 93 | "end" 94 | ], 95 | "prefix": "on_message(self, message_id, message, sender)", 96 | "description": "Defold script on_message() function" 97 | }, 98 | { 99 | "body": [ 100 | "---@package", 101 | "---@param self ${1:${TM_FILENAME}}", 102 | "---@param action_id hash", 103 | "---@param action on_input.action", 104 | "---@return boolean|nil", 105 | "function on_input(self, action_id, action)", 106 | "\t${2}", 107 | "end" 108 | ], 109 | "prefix": "on_input(self, action_id, action)", 110 | "description": "Defold script on_input() function" 111 | }, 112 | { 113 | "body": [ 114 | "---@package", 115 | "---@param self ${1:${TM_FILENAME}}", 116 | "function final(self)", 117 | "\t${2}", 118 | "end" 119 | ], 120 | "prefix": "final(self)", 121 | "description": "Defold script final() function" 122 | }, 123 | { 124 | "body": [ 125 | "---@package", 126 | "---@param self ${1:${TM_FILENAME}}", 127 | "function on_reload(self)", 128 | "\t${2}", 129 | "end" 130 | ], 131 | "prefix": "on_reload(self)", 132 | "description": "Defold script on_reload() function" 133 | } 134 | ] 135 | } -------------------------------------------------------------------------------- /src/migration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as config from './config' 3 | import * as utils from './utils' 4 | import * as momento from './momento' 5 | import * as extensions from './data/extensions' 6 | import * as debuggers from './data/debuggers' 7 | import log from './logger' 8 | 9 | export async function migrateGlobal(fromVersion: string) { 10 | if (fromVersion == "2.0.5") { 11 | await migrateGlobalFrom205() 12 | } 13 | 14 | if (utils.compareVersions(fromVersion, '2.1.7') <= 0) { 15 | await migrateGlobalFrom217() 16 | } else { 17 | log(`Nothing to migrate globally, skipped.`) 18 | } 19 | 20 | await momento.setLastGlobalMigrationVersion(config.extension.version) 21 | } 22 | 23 | export async function migrateWorkspace(fromVersion: string) { 24 | if (fromVersion == '2.0.5') { 25 | await migrateWorkspaceFrom205() 26 | } 27 | 28 | if (utils.compareVersions(fromVersion, '2.0.7') <= 0) { 29 | await migrateWorkspaceFrom207() 30 | } else { 31 | log(`Nothing to migrate in the workspace, skipped.`) 32 | } 33 | 34 | await momento.setLastWorkspaceMigrationVersion(config.extension.version) 35 | } 36 | 37 | async function migrateGlobalFrom205() { 38 | const oldKeys = [ 39 | 'defold.general.editorPath', 40 | 'defold.general.suggestSetup', 41 | 'defold.general.showBobOutput', 42 | 'defold.annotations.repository', 43 | 'defold.dependencies.email', 44 | 'defold.dependencies.authToken', 45 | 'defold.bundle.ios.debug.provisioningProfile', 46 | 'defold.bundle.ios.debug.identity', 47 | 'defold.bundle.ios.release.provisioningProfile', 48 | 'defold.bundle.ios.release.identity', 49 | 'defold.bundle.android.keystore', 50 | 'defold.bundle.android.keystorePass', 51 | 'defold.bundle.android.keystoreAlias' 52 | ] 53 | 54 | for (const oldKey of oldKeys) { 55 | const value = vscode.workspace.getConfiguration().get(oldKey) 56 | 57 | if (value == undefined) { 58 | continue 59 | } 60 | 61 | const newKey = oldKey.replace('defold.', 'defoldKit.') 62 | 63 | try { 64 | log(`Migrating settings value from ${oldKey} to ${newKey}`) 65 | await vscode.workspace.getConfiguration().update(newKey, value, true) 66 | } catch (error) { 67 | log(`${error}`, { openOutput: true }) 68 | } 69 | } 70 | } 71 | 72 | async function migrateWorkspaceFrom205() { 73 | const launchSettings = vscode.workspace.getConfiguration('launch') 74 | const configurations = launchSettings.get('configurations') as vscode.DebugConfiguration[] 75 | 76 | const configuration = configurations.find(existingConfiguration => { 77 | return existingConfiguration.name == 'Defold' && existingConfiguration.type == 'lua-local' 78 | }) 79 | 80 | if (configuration) { 81 | log(`Migrating the launch configuration from 2.0.5`) 82 | 83 | configuration.name = debuggers.recommended[extensions.ids.localLuaDebugger].name 84 | configuration.preLaunchTask = debuggers.recommended[extensions.ids.localLuaDebugger].preLaunchTask 85 | 86 | await launchSettings.update('configurations', configurations) 87 | } 88 | } 89 | 90 | async function migrateWorkspaceFrom207() { 91 | const launchSettings = vscode.workspace.getConfiguration('launch') 92 | const configurations = launchSettings.get('configurations') as vscode.DebugConfiguration[] 93 | 94 | const configuration = configurations.find(existingConfiguration => { 95 | return existingConfiguration.name == 'Defold Kit' && existingConfiguration.type == 'lua-local' 96 | }) 97 | 98 | if (configuration) { 99 | log(`Migrating the launch configuration from 2.0.7`) 100 | 101 | configuration.program = debuggers.recommended[extensions.ids.localLuaDebugger].program 102 | configuration.args = debuggers.recommended[extensions.ids.localLuaDebugger].args 103 | configuration.windows.program = debuggers.recommended[extensions.ids.localLuaDebugger].windows.program 104 | configuration.windows.args = debuggers.recommended[extensions.ids.localLuaDebugger].windows.args 105 | 106 | await launchSettings.update('configurations', configurations) 107 | } 108 | } 109 | 110 | async function migrateGlobalFrom217() { 111 | const annotationsRepositoryKey = `defoldKit.annotations.repository` 112 | const outdatedValue = `d954mas/defold-api-emmylua` 113 | const currentValue = vscode.workspace.getConfiguration().get(annotationsRepositoryKey) 114 | 115 | if (currentValue == outdatedValue) { 116 | log(`Fallback the settings '${annotationsRepositoryKey}' to the default value because it is too outdated.`) 117 | 118 | try { 119 | await vscode.workspace.getConfiguration().update(annotationsRepositoryKey, undefined, true) 120 | } catch (error) { 121 | log(`${error}`, { openOutput: true }) 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the Defold Kit extension will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | 7 | ## [2.1.10] - 2024-12-30 8 | 9 | ### Fixed 10 | 11 | - Fixed extracting the `dmengine.exe` for Defold 1.9.6 on Windows [#50](https://github.com/astrochili/vscode-defold/issues/50). 12 | 13 | ## [2.1.9] - 2024-08-11 14 | 15 | ### Added 16 | 17 | - Added missing check for library folder changes at startup [#47](https://github.com/astrochili/vscode-defold/issues/47). 18 | 19 | ## [2.1.8] - 2024-08-11 20 | 21 | ### Added 22 | 23 | - Added `Lua.runtime.pathStrict: true` with additional fixes for annotation paths so that autocompletion of libraries works. 24 | - Added comparing Defold and annotations versions to sync Defold API automatically. Turned on by default. 25 | - Added watching on the `.internal/libs` folder changes to sync libraries annotations automatically [#45](https://github.com/astrochili/vscode-defold/issues/45). Turned on by default. 26 | 27 | ### Fixed 28 | 29 | - Fixed excessive nesting of annotation folders (`defold_api/defold_api` and `libs_api/extension_v1/extension/...`). 30 | 31 | ### Removed 32 | 33 | - Removed `d954mas/defold-api-emmylua` from the annotation sources because it's almost a year out of date. 34 | 35 | ## [2.1.7] - 2024-08-05 36 | 37 | ### Removed 38 | 39 | - Removed `Lua.runtime.pathStrict: true` from the `settings.json` template because of the bad side effect on external libraries autocompletion. Will review it later. 40 | 41 | ## [2.1.6] - 2024-07-07 42 | 43 | ### Added 44 | 45 | - Added `Lua.runtime.pathStrict': true` to the `settings.json` template. 46 | 47 | ### Fixed 48 | 49 | - Fixed cleaning outdated and doubled library annotations after deletion or updating the version. 50 | - Fixed cleaning library annotations folder with the `Clean Annotations` command. 51 | 52 | ### Removed 53 | 54 | - Removed `Lua.telemetry.enable` from the `settings.json` template. 55 | 56 | ## [2.1.5] - 2024-06-12 57 | 58 | ### Updated 59 | 60 | - Improved the snippets a bit. 61 | 62 | ### Note 63 | 64 | - The definition of the `on_input.action` class exists in the updated [defold-annotations](https://github.com/astrochili/defold-annotations/releases) release. So to make `on_input.action` available, please sync [Defold API annotations](https://github.com/astrochili/vscode-defold?tab=readme-ov-file#annotations-syncing) via command pallete. 65 | 66 | ## [2.1.4] - 2024-06-11 67 | 68 | ### Added 69 | 70 | - Added a `script` snippet with a `self` class definition and annotated lifecycle functions. Useful to create a script file with ready to use `self` annotations. By default, the class is named by filename. 71 | - Added annotations and the `---@package` marker for script lifecycle snippets. The `---@package` allows to hide lifecycle functions from the global context so that you don't get confused when choosing between a snippet and a real declared function with the same name. 72 | 73 | ## [2.1.3] - 2024-04-10 74 | 75 | ### Added 76 | 77 | - Added YAML files associations [#40](https://github.com/astrochili/vscode-defold/issues/40) 78 | 79 | ## [2.1.2] - 2024-01-24 80 | 81 | ### Fixed 82 | 83 | - Fixed the libraries API syncing on Windows [#37](https://github.com/astrochili/vscode-defold/issues/37) 84 | 85 | ## [2.1.1] - 2023-11-11 86 | 87 | ### Changed 88 | 89 | - Optional extensions are now unselected during setup by default. Most people obviously don't need them. 90 | 91 | ### Fixed 92 | 93 | - Fixed confusing false start of an old build, in case of new build error. Now the old build is deleted in this case to avoid accidental launch. 94 | 95 | ## [2.1.0] - 2023-10-19 96 | 97 | ### Added 98 | 99 | - Added the `Open Defold` command to open the current project in the Defold Editor. 100 | 101 | ## [2.0.8] - 2023-10-10 102 | 103 | ### Changed 104 | 105 | - Changed the build path to avoid conflicts with the Defold Editor build. 106 | 107 | ## [2.0.7] - 2023-10-03 108 | 109 | ### Fixed 110 | 111 | - Fixed the soft migration of the workspace launch configuration. 112 | 113 | ## [2.0.6] - 2023-10-03 114 | 115 | ### Changed 116 | 117 | - Renamed the category of tasks and commands from `Defold` to `Defold Kit` to avoid conflicts. 118 | - Renamed all the settings keys from `defold.` to `defoldKit.` to avoid conflicts. 119 | - Renamed launch configuration to `Defold Kit` to avoid conflicts. 120 | 121 | ### Added 122 | 123 | - Soft migration of user settings from `defold.` to `defoldKit.` keys. 124 | - Soft migration of the workspace launch configuration from `Defold` to `Defold Kit` name. 125 | 126 | ## [2.0.5] - 2023-09-30 127 | 128 | ### Removed 129 | 130 | - Removed everything related the `Launch (without debugger)` and `Build to Launch` commands to avoid confusing. 131 | - Now the only way to launch the game is using the `Run ang Debug` panel. 132 | 133 | ## [2.0.4] - 2023-09-30 134 | 135 | ### Fixed 136 | 137 | - Fixed the launch command for a project with spaces in the path. 138 | 139 | ## [2.0.3] - 2023-09-26 140 | 141 | ### Added 142 | 143 | - Added an additional annotations source to the settings — [d954mas/defold-api-emmylua](https://github.com/d954mas/defold-api-emmylua). 144 | 145 | ## [2.0.2] - 2023-09-22 146 | 147 | ### Fixed 148 | 149 | - Fixed `dmengine` selection bug for macOS with Intel during preparing for launch. 150 | 151 | 152 | ## [2.0.1] - 2023-09-19 153 | 154 | ### Fixed 155 | 156 | - Unwrapped `require('debugger.debugger’)` calls in the docs and in the `debugger.script`file to avoid problems with Defold code analyser before build 157 | 158 | ## [2.0.0] - 2023-09-18 159 | 160 | - Initial release -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | import * as momento from './momento' 14 | import * as utils from './utils' 15 | import * as commands from './commands' 16 | import * as tasks from './tasks' 17 | import * as migration from './migration' 18 | import * as wizard from './wizard' 19 | import * as annotations from './annotations' 20 | import log from './logger' 21 | 22 | let runningCommand: string | undefined 23 | 24 | export async function activate(context: vscode.ExtensionContext) { 25 | const workspaceFolder = vscode.workspace.workspaceFolders?.at(0) 26 | if (!workspaceFolder) { 27 | log(`Cannot activate the extension because the workspace folder is not found`) 28 | return 29 | } 30 | 31 | const workspaceStoragePath = context.storageUri?.fsPath 32 | if (!workspaceStoragePath) { 33 | log(`Cannot activate the extension because the workspace storage folder is not found`) 34 | return 35 | } 36 | 37 | const globalStoragePath = context.globalStorageUri?.fsPath 38 | if (!globalStoragePath) { 39 | log(`Cannot activate the extension because the global storage folder is not found`) 40 | return 41 | } 42 | 43 | await config.init(context, workspaceFolder, workspaceStoragePath, globalStoragePath) 44 | 45 | for (const command of Object.keys(commands)) { 46 | const action = commands[command as keyof typeof commands] 47 | const commandId = `${config.extension.commandPrefix}.${command}` 48 | 49 | context.subscriptions.push(vscode.commands.registerCommand(commandId, async () => { 50 | if (!await utils.isPathExists(config.paths.workspaceGameProject)) { 51 | vscode.window.showWarningMessage(`Doesn't look like a Defold project`) 52 | log(`A user tried to run command '${commandId}' without Defold project`) 53 | return 54 | } 55 | 56 | if (runningCommand) { 57 | vscode.window.showWarningMessage(`Cannot run the command '${commandId}' until another command '${runningCommand}' is finished`) 58 | log(`A user tried to run command '${commandId}' during another command '${runningCommand}'.`) 59 | return 60 | } 61 | 62 | log(`Command '${commandId}' is called`) 63 | runningCommand = commandId 64 | 65 | try { 66 | await config.init(context, workspaceFolder, workspaceStoragePath, globalStoragePath) 67 | await action() 68 | } catch (error) { 69 | vscode.window.showWarningMessage(`Unexpected error occured during running the command '${commandId}'. See Output for details.`) 70 | log(`Unhandled exception during running the command '${commandId}': ${error}}`) 71 | } 72 | 73 | runningCommand = undefined 74 | log(`Command '${commandId}' is finished`) 75 | 76 | // Need to return a string to use it as a preLaunchTask 77 | return '' 78 | })) 79 | } 80 | 81 | vscode.tasks.registerTaskProvider(config.extension.taskPrefix, { 82 | provideTasks: () => { 83 | return tasks.makeTasks() 84 | }, 85 | resolveTask(_task: vscode.Task): vscode.Task | undefined { 86 | return undefined 87 | } 88 | }) 89 | 90 | const lastGlobalMigrationVersion = momento.getLastGlobalMigrationVersion() 91 | if (lastGlobalMigrationVersion != config.extension.version) { 92 | log(`Starting global migration from ${lastGlobalMigrationVersion} to ${config.extension.version}.`) 93 | await migration.migrateGlobal(lastGlobalMigrationVersion) 94 | } else { 95 | log(`Last global migration version is ${lastGlobalMigrationVersion}. No need to migrate.`) 96 | } 97 | 98 | const lastWorkspaceMigrationVersion = momento.getLastWorkspaceMigrationVersion() 99 | if (lastWorkspaceMigrationVersion != config.extension.version) { 100 | log(`Starting workspace migration from ${lastWorkspaceMigrationVersion} to ${config.extension.version}.`) 101 | await migration.migrateWorkspace(lastWorkspaceMigrationVersion) 102 | } else { 103 | log(`Last workspace migration version is ${lastWorkspaceMigrationVersion}. No need to migrate.`) 104 | } 105 | 106 | log(`Extension '${config.extension.displayName}' is activated`) 107 | 108 | if (await wizard.suggestSetupIfApplicable()) { 109 | // No need to continue because there is a setup suggestion on the screen 110 | return 111 | } 112 | 113 | if (await utils.isPathExists(config.paths.workspaceGameProject)) { 114 | const annotationsVersion = momento.getAnnotationsVersion() 115 | const defoldVersion = config.defold?.version 116 | 117 | log(`Defold Editor version is ${defoldVersion}`) 118 | log(`Defold API annotations version is ${annotationsVersion ?? 'unknown'}`) 119 | 120 | if (utils.settingsBoolean(config.settingsKeys.annotationsAutoSyncDefold)) { 121 | if (defoldVersion && defoldVersion != annotationsVersion) { 122 | log(`Let's try to sync Defold API annotations to ${defoldVersion}`) 123 | 124 | await vscode.window.withProgress({ 125 | location: vscode.ProgressLocation.Notification, 126 | title: 'Syncing annotations' 127 | }, async progress => { 128 | progress.report({ message: 'Defold API...' }) 129 | await annotations.syncDefoldAnnotations(defoldVersion) 130 | }) 131 | } else { 132 | log(`No need to sync Defold API annotations`) 133 | } 134 | } else { 135 | log(`Auto syncing for Defold API annotations is turned off`) 136 | } 137 | 138 | if (utils.settingsBoolean(config.settingsKeys.annotationsAutoSyncLibs)) { 139 | annotations.startWatchingLibsToSyncAnnotations() 140 | } 141 | } 142 | } 143 | 144 | export function deactivate() { 145 | log(`Extension '${config.extension.displayName}' is deactivated`) 146 | } -------------------------------------------------------------------------------- /src/bob.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | import { DefoldConfiguration } from './config' 14 | import * as utils from './utils' 15 | import * as shell from './shell' 16 | import log from './logger' 17 | import path = require('path') 18 | 19 | async function runBob(defold: DefoldConfiguration, args: string[]): Promise { 20 | const executable = `"${defold.javaBin}"` 21 | const bobArgs = [`-cp`, `"${defold.editorJar}"`, `"${config.constants.bobClass}"`].concat(args) 22 | 23 | if (utils.settingsBoolean(config.settingsKeys.showBobOutput)) { 24 | log(`->`, { openOutput: true }) 25 | log(`-> The Output pane automatically opened by running bob.jar`) 26 | log(`-> You can turn this off in ${config.extension.displayName} settings.`) 27 | log(`->`) 28 | } 29 | 30 | const result = await shell.execute('Bob', executable, bobArgs) 31 | return result.success 32 | } 33 | 34 | export async function cleanBuild(defold: DefoldConfiguration): Promise { 35 | let args = new Array 36 | 37 | args.push(`--output`, config.paths.relativeBuildLauncher) 38 | args.push(`distclean`) 39 | 40 | return await runBob(defold, args) 41 | } 42 | 43 | export async function resolve( 44 | defold: DefoldConfiguration, 45 | options?: { email?: string, auth?: string } 46 | ): Promise { 47 | let args = new Array 48 | 49 | if (options?.email) { 50 | args.push(`--email`, options.email) 51 | } 52 | 53 | if (options?.auth) { 54 | args.push(`--auth`, options.auth) 55 | } 56 | 57 | args.push(`--output`, config.paths.relativeBuildLauncher) 58 | args.push(`resolve`) 59 | 60 | return await runBob(defold, args) 61 | } 62 | 63 | export async function build(defold: DefoldConfiguration): Promise { 64 | let args = new Array 65 | 66 | args.push(`--variant`, `debug`) 67 | args.push(`--output`, config.paths.relativeBuildLauncher) 68 | args.push(`build`) 69 | 70 | return await runBob(defold, args) 71 | } 72 | 73 | interface BundleOptions { 74 | target: string, 75 | email?: string, 76 | auth?: string, 77 | release: boolean, 78 | textureCompression: boolean, 79 | buildReport: boolean, 80 | debugSymbols: boolean, 81 | liveUpdate: boolean, 82 | iosMobileProvisioning?: string, 83 | iosIdentity?: string, 84 | androidKeystore?: string, 85 | androidKeystorePass?: string, 86 | androidKeystoreAlias?: string, 87 | androidBundleFormat?: string 88 | } 89 | 90 | export async function bundle(defold: DefoldConfiguration, options: BundleOptions): Promise { 91 | let args = new Array 92 | 93 | const bundlePath = path.join(config.paths.workspace, `bundle`, options.target) 94 | 95 | await utils.deleteFile(bundlePath) 96 | await utils.createDirectory(bundlePath) 97 | 98 | const targetInfo = config.bundleTargets[options.target as keyof typeof config.bundleTargets] 99 | 100 | if (!targetInfo) { 101 | log(`Can't find platform and architectures for target ${options.target}`) 102 | return 103 | } 104 | 105 | args.push(`--output`, config.paths.relativeBuildLauncher) 106 | args.push(`--archive`) 107 | args.push(`--platform`, targetInfo.platform) 108 | args.push(`--architectures`, targetInfo.architectures) 109 | args.push(`--bundle-output`, bundlePath) 110 | 111 | if (options.buildReport) { 112 | const reportPath = path.join(bundlePath, `build-report.html`) 113 | args.push(`--build-report-html`, reportPath) 114 | } 115 | 116 | if (options.email) { 117 | args.push(`--email`, options.email) 118 | } 119 | 120 | if (options.auth) { 121 | args.push(`--auth`, options.auth) 122 | } 123 | 124 | if (options.textureCompression) { 125 | args.push(`--texture-compression`) 126 | } 127 | 128 | if (options.liveUpdate) { 129 | args.push(`--liveupdate`, `yes`) 130 | } 131 | 132 | if (options.debugSymbols) { 133 | args.push(`--with-symbols`) 134 | } 135 | 136 | if (targetInfo == config.bundleTargets.iOS) { 137 | if (!options.iosMobileProvisioning) { 138 | vscode.window.showWarningMessage(`Mobile Provisioning Profile for ${options.release ? 'Release' : 'Debug'} variant is required to bundle for iOS. Set up it in ${config.extension.displayName} settings.`) 139 | log('Bundle cancelled because no iOS Mobile Provisioning Profile') 140 | return 141 | } 142 | 143 | args.push(`--mobileprovisioning`, options.iosMobileProvisioning) 144 | 145 | if (!options.iosIdentity) { 146 | vscode.window.showWarningMessage(`Signing Identity is required to bundle for iOS. Set up it in ${config.extension.displayName} settings.`) 147 | log('Bundle cancelled because no iOS Signing Identity') 148 | return 149 | } 150 | 151 | args.push(`--identity`, options.iosIdentity) 152 | } else if (targetInfo == config.bundleTargets.Android) { 153 | if (options.androidKeystore) { 154 | args.push(`--keystore`, options.androidKeystore) 155 | } 156 | 157 | if (options.androidKeystorePass) { 158 | args.push(`--keystore-pass`, options.androidKeystorePass) 159 | } 160 | 161 | if (options.androidKeystoreAlias) { 162 | args.push(`--keystore-alias`, options.androidKeystoreAlias) 163 | } 164 | 165 | if (options.androidBundleFormat) { 166 | args.push(`--bundle-format`, options.androidBundleFormat) 167 | } 168 | } 169 | 170 | args.push(`resolve`, `distclean`, `build`, `bundle`) 171 | 172 | const success = await runBob(defold, args) 173 | return success ? bundlePath : undefined 174 | } -------------------------------------------------------------------------------- /src/launcher.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | import { DefoldConfiguration } from './config' 14 | import * as utils from './utils' 15 | import * as shell from './shell' 16 | import log from './logger' 17 | import path = require('path') 18 | 19 | async function extractFromDefold(defold: DefoldConfiguration, internalPath: string, destinationPath: string): Promise { 20 | const tempPath = path.join(config.paths.workspaceStorage, 'tmp') 21 | 22 | log(`Creating temporary directory: ${tempPath}`) 23 | if (!await utils.createDirectory(tempPath)) { 24 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 25 | return false 26 | } 27 | 28 | const result = await shell.execute( 29 | `Jar Extracting`, 30 | `"${defold.jarBin}"`, 31 | [`-xf`, `"${defold.editorJar}"`, `"${internalPath}"`], 32 | tempPath 33 | ) 34 | 35 | if (!result.success) { 36 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 37 | return false 38 | } 39 | 40 | log(`Copying '${internalPath}' to '${destinationPath}'`) 41 | const isCopied = await utils.copy(path.join(tempPath, internalPath), destinationPath) 42 | 43 | if (!isCopied) { 44 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 45 | return false 46 | } 47 | 48 | log(`Deleting temporary directory: ${tempPath}`) 49 | await utils.deleteFile(tempPath) 50 | 51 | return true 52 | } 53 | 54 | export async function prepare(defold: DefoldConfiguration): Promise { 55 | const launchConfig = config.launch.configs[process.platform] 56 | 57 | if (!launchConfig) { 58 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 59 | log(`Failed to get launch configuration for the unknown platform ${process.platform}`) 60 | return false 61 | } 62 | 63 | const buildPlatform = launchConfig.buildPlatform 64 | const engineExecutable = launchConfig.executable 65 | const libexecBinPath = launchConfig.libexecBinPath 66 | 67 | log(`Process platform: '${process.platform}'`) 68 | log(`Build platform: '${buildPlatform}'`) 69 | 70 | const buildPlatformPath = path.join(config.paths.workspaceBuild, buildPlatform) 71 | const enginePlatformPath = path.join(buildPlatformPath, engineExecutable) 72 | 73 | const engineLauncherPath = path.join(config.paths.workspaceBuildLauncher, engineExecutable) 74 | 75 | const isEngineHere = await utils.isPathExists(enginePlatformPath) 76 | 77 | if (isEngineHere) { 78 | log(`Native engine found. Copying the engine from '${enginePlatformPath}' to '${engineLauncherPath}'`) 79 | const isCopied = await utils.copy(enginePlatformPath, engineLauncherPath) 80 | 81 | if (!isCopied) { 82 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 83 | log(`Failed to copy the engine executable from the build`) 84 | return false 85 | } 86 | } else { 87 | log(`Extracting the vanilla engine from Defold to '${engineLauncherPath}'`) 88 | const isExctracted = await extractFromDefold( 89 | defold, 90 | path.join(libexecBinPath, engineExecutable), 91 | engineLauncherPath 92 | ) 93 | 94 | if (!isExctracted) { 95 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 96 | log(`Failed to extract the engine executable from Defold'`) 97 | return false 98 | } 99 | } 100 | 101 | if (launchConfig.requiredFiles) { 102 | for (const requiredFile of launchConfig.requiredFiles) { 103 | const destinationPath = path.join(config.paths.workspaceBuildLauncher, requiredFile) 104 | 105 | if (await utils.isPathExists(destinationPath)) { 106 | log(`No need to extract '${requiredFile}', already have`) 107 | } else { 108 | log(`Extracting required '${requiredFile}' from Defold`) 109 | const isExctracted = await extractFromDefold( 110 | defold, 111 | path.join(libexecBinPath, requiredFile), 112 | destinationPath, 113 | ) 114 | 115 | if (!isExctracted) { 116 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 117 | log(`Failed to extract '${requiredFile}' from Defold`) 118 | return false 119 | } 120 | } 121 | } 122 | } 123 | 124 | if (utils.isMac || utils.isLinux) { 125 | log(`Unix: Make the engine executable`) 126 | const result = await shell.execute( 127 | 'Chmod', 128 | 'chmod', 129 | [`+x`, `"${engineLauncherPath}"`] 130 | ) 131 | 132 | if (!result.success) { 133 | vscode.window.showErrorMessage(`Failed preparing to launch. See Output for details.`) 134 | log(`Failed to make the engine executable`) 135 | return false 136 | } 137 | } 138 | 139 | return true 140 | } 141 | 142 | export async function openDefold(defold: DefoldConfiguration) { 143 | let result 144 | 145 | if (utils.isWindows) { 146 | result = await shell.execute('Defold', `"${config.paths.workspaceGameProject}"`) 147 | } else if (utils.isMac) { 148 | if (await utils.isProcessRunning(config.defoldProcess)) { 149 | result = await shell.execute('Defold', 'osascript', ['-e', `'activate application "Defold"'`]) 150 | } else { 151 | result = await shell.execute('Defold', 'open', [`'${defold.editorPath}'`, `'${config.paths.workspaceGameProject}'`]) 152 | } 153 | } else if (utils.isLinux) { 154 | result = await shell.execute('Defold', 'xdg-open', [`"${config.paths.workspaceGameProject}"`]) 155 | } else { 156 | log(`Unable to open Defold Editor due to unknown platform: ${process.platform}`) 157 | } 158 | 159 | if (!result || !result.success) { 160 | vscode.window.showErrorMessage(`Failed to open Defold Editor. See Output for details.`) 161 | } 162 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "astronachos", 3 | "name": "defold", 4 | "contributesPrefix": "defoldKit", 5 | "version": "2.1.10", 6 | "license": "MIT", 7 | "displayName": "Defold Kit", 8 | "description": "Toolkit to build, launch, debug, bundle and deploy your game made with Defold", 9 | "icon": "images/icon.png", 10 | "galleryBanner": { 11 | "color": "#202428", 12 | "theme": "dark" 13 | }, 14 | "keywords": [ 15 | "defold", 16 | "lua" 17 | ], 18 | "categories": [ 19 | "Snippets", 20 | "Debuggers", 21 | "Extension Packs", 22 | "Other" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/astrochili/vscode-defold" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/astrochili/vscode-defold/issues" 30 | }, 31 | "sponsor": { 32 | "url": "https://buymeacoffee.com/astrochili" 33 | }, 34 | "engines": { 35 | "vscode": "^1.79.0" 36 | }, 37 | "activationEvents": [ 38 | "workspaceContains:game.project" 39 | ], 40 | "main": "./out/extension.js", 41 | "contributes": { 42 | "snippets": [ 43 | { 44 | "language": "lua", 45 | "path": "./snippets/defold.json" 46 | } 47 | ], 48 | "commands": [ 49 | { 50 | "command": "defoldKit.setup", 51 | "category": "Defold Kit", 52 | "icon": "$(tools)", 53 | "title": "Setup", 54 | "enablement": "workspaceFolderCount > 0" 55 | }, 56 | { 57 | "command": "defoldKit.syncAnnotations", 58 | "category": "Defold Kit", 59 | "icon": "$(sync)", 60 | "title": "Sync API Annotations", 61 | "enablement": "workspaceFolderCount > 0" 62 | }, 63 | { 64 | "command": "defoldKit.cleanAnnotations", 65 | "category": "Defold Kit", 66 | "icon": "$(trash)", 67 | "title": "Clean API Annotations", 68 | "enablement": "workspaceFolderCount > 0" 69 | }, 70 | { 71 | "command": "defoldKit.openDefold", 72 | "category": "Defold Kit", 73 | "icon": "$(window)", 74 | "title": "Open Defold", 75 | "enablement": "workspaceFolderCount > 0" 76 | }, 77 | { 78 | "command": "defoldKit.cleanBuild", 79 | "category": "Defold Kit", 80 | "icon": "$(symbol-event)", 81 | "title": "Clean Build", 82 | "enablement": "workspaceFolderCount > 0" 83 | }, 84 | { 85 | "command": "defoldKit.resolve", 86 | "category": "Defold Kit", 87 | "icon": "$(library)", 88 | "title": "Resolve Dependencies", 89 | "enablement": "workspaceFolderCount > 0" 90 | }, 91 | { 92 | "command": "defoldKit.bundle", 93 | "category": "Defold Kit", 94 | "icon": "$(briefcase)", 95 | "title": "Bundle", 96 | "enablement": "workspaceFolderCount > 0" 97 | }, 98 | { 99 | "command": "defoldKit.deploy", 100 | "category": "Defold Kit", 101 | "icon": "$(device-mobile)", 102 | "title": "Deploy to Mobile", 103 | "enablement": "workspaceFolderCount > 0" 104 | }, 105 | { 106 | "command": "defoldKit.build", 107 | "category": "Defold Kit", 108 | "icon": "$(package)", 109 | "title": "Build", 110 | "enablement": "false" 111 | } 112 | ], 113 | "configuration": [ 114 | { 115 | "title": "Defold Kit", 116 | "properties": { 117 | "defoldKit.general.editorPath": { 118 | "order": 0, 119 | "type": "string", 120 | "default": "", 121 | "description": "Path to the Defold Editor" 122 | }, 123 | "defoldKit.general.suggestSetup": { 124 | "order": 1, 125 | "type": "boolean", 126 | "default": true, 127 | "markdownDescription": "Suggest to setup Defold Kit if the `game.project` file is found" 128 | }, 129 | "defoldKit.general.showBobOutput": { 130 | "order": 2, 131 | "type": "boolean", 132 | "default": true, 133 | "markdownDescription": "Open the Output panel when the `bob.jar` is executing" 134 | }, 135 | "defoldKit.annotations.repository": { 136 | "order": 3, 137 | "type": "string", 138 | "default": "astrochili/defold-annotations", 139 | "enum": [ 140 | "astrochili/defold-annotations", 141 | "mikatuo/defold-lua-annotations" 142 | ], 143 | "description": "Which Github repository to use to fetch Defold API annotations" 144 | }, 145 | "defoldKit.annotations.autosync.defold": { 146 | "order": 4, 147 | "type": "boolean", 148 | "default": true, 149 | "description": "Sync annotations for Defold API automatically at the extension launch" 150 | }, 151 | "defoldKit.annotations.autosync.libs": { 152 | "order": 5, 153 | "type": "boolean", 154 | "default": true, 155 | "markdownDescription": "Sync annotations for dependencies automatically by watching `.internal/libs` folder" 156 | }, 157 | "defoldKit.dependencies.email": { 158 | "order": 6, 159 | "type": "string", 160 | "default": "", 161 | "description": "User email to resolve dependencies" 162 | }, 163 | "defoldKit.dependencies.authToken": { 164 | "order": 7, 165 | "type": "string", 166 | "default": "", 167 | "description": "Authentication token to resolve dependencies" 168 | }, 169 | "defoldKit.bundle.ios.debug.provisioningProfile": { 170 | "order": 8, 171 | "type": "string", 172 | "default": "", 173 | "markdownDescription": "Path to the `*.mobileprovision profile` for the Debug variant" 174 | }, 175 | "defoldKit.bundle.ios.debug.identity": { 176 | "order": 9, 177 | "type": "string", 178 | "default": "", 179 | "markdownDescription": "Code signing identity for `#defoldKit.bundle.ios.debug.provisioningProfile#`" 180 | }, 181 | "defoldKit.bundle.ios.release.provisioningProfile": { 182 | "order": 10, 183 | "type": "string", 184 | "default": "", 185 | "markdownDescription": "Path to the `*.mobileprovision` profile for the Release variant" 186 | }, 187 | "defoldKit.bundle.ios.release.identity": { 188 | "order": 11, 189 | "type": "string", 190 | "default": "", 191 | "markdownDescription": "Code signing identity for `#defoldKit.bundle.ios.release.provisioningProfile#`" 192 | }, 193 | "defoldKit.bundle.android.keystore": { 194 | "order": 12, 195 | "type": "string", 196 | "default": "", 197 | "markdownDescription": "Path to the `*.keystore` file" 198 | }, 199 | "defoldKit.bundle.android.keystorePass": { 200 | "order": 13, 201 | "type": "string", 202 | "default": "", 203 | "markdownDescription": "Path to the `*.keystore.pass.txt` file" 204 | }, 205 | "defoldKit.bundle.android.keystoreAlias": { 206 | "order": 14, 207 | "type": "string", 208 | "default": "", 209 | "markdownDescription": "Name of the alias from the `#defoldKit.bundle.android.keystore#`" 210 | } 211 | } 212 | } 213 | ] 214 | }, 215 | "scripts": { 216 | "vscode:prepublish": "npm run esbuild-base -- --minify", 217 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", 218 | "esbuild": "npm run esbuild-base -- --sourcemap", 219 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch" 220 | }, 221 | "devDependencies": { 222 | "@types/adm-zip": "^0.5.0", 223 | "@types/glob": "^8.1.0", 224 | "@types/mocha": "^10.0.1", 225 | "@types/node": "20.2.5", 226 | "@types/vscode": "^1.79.0", 227 | "@typescript-eslint/eslint-plugin": "^5.59.8", 228 | "@typescript-eslint/parser": "^5.59.8", 229 | "@vscode/test-electron": "^2.3.2", 230 | "eslint": "^8.41.0", 231 | "glob": "^8.1.0", 232 | "mocha": "^10.2.0", 233 | "typescript": "^5.1.3" 234 | }, 235 | "dependencies": { 236 | "adm-zip": "0.5.10", 237 | "axios": "1.5.0", 238 | "ini": "4.1.1", 239 | "json5": "2.2.3" 240 | } 241 | } -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as config from './config' 13 | import * as momento from './momento' 14 | import * as wizard from './wizard' 15 | import * as annotations from './annotations' 16 | import * as extensions from './data/extensions' 17 | import * as bob from './bob' 18 | import * as launcher from './launcher' 19 | import * as deployer from './deployer' 20 | import * as utils from './utils' 21 | import log from './logger' 22 | 23 | export async function setup() { 24 | const defold = await wizard.offerSelectDefoldPath() 25 | 26 | if (!defold) { 27 | log(`Setup is cancelled during the Defold Editor's path dialog`) 28 | return 29 | } 30 | 31 | const installedExtensionIds = await wizard.offerInstallExtensions() 32 | if (!installedExtensionIds) { 33 | log('Setup is cancelled during the extensions installation dialog') 34 | return 35 | } 36 | 37 | const areSettingsApplyed = await wizard.offerApplySettings(installedExtensionIds) 38 | if (!areSettingsApplyed) { 39 | log('Setup is cancelled during the applying settings dialog') 40 | return 41 | } 42 | 43 | if (installedExtensionIds.includes(extensions.ids.luaLanguageServer) || vscode.extensions.getExtension(extensions.ids.luaLanguageServer)) { 44 | const areAnnotationsSynced = await wizard.offerSyncAnnotations(defold.version) 45 | if (!areAnnotationsSynced) { 46 | log('Setup is cancelled during the annotations syncing dialog') 47 | return 48 | } 49 | } else { 50 | log(`There is no '${extensions.ids.luaLanguageServer}' extensions installed so the annotations syncyng is skipped`) 51 | } 52 | 53 | momento.setOnceSetup(true) 54 | vscode.window.showInformationMessage(`${config.extension.displayName} setup has been finished`) 55 | } 56 | 57 | export async function syncAnnotations() { 58 | const defold = config.defold 59 | 60 | if (!defold) { 61 | return wizard.suggestSetup(`Syncing API annotations requires to setup ${config.extension.displayName} first`) 62 | } 63 | 64 | if (!vscode.extensions.getExtension(extensions.ids.luaLanguageServer)) { 65 | return wizard.suggestSetup(`Syncing API annotations requires to install the '${extensions.ids.luaLanguageServer}' extension first`) 66 | } 67 | 68 | const areAnnotationsSynced = await wizard.offerSyncAnnotations(defold.version, 'Syncing API annotations') 69 | 70 | if (areAnnotationsSynced) { 71 | vscode.window.showInformationMessage(`Syncing API annotations completed`) 72 | } 73 | } 74 | 75 | export async function cleanAnnotations() { 76 | const defold = config.defold 77 | 78 | if (!defold) { 79 | return wizard.suggestSetup(`Cleaning API annotations requires to setup ${config.extension.displayName} first`) 80 | } 81 | 82 | const isClean = await annotations.cleanAnnotations() 83 | 84 | if (isClean) { 85 | vscode.window.showInformationMessage(`Cleaning API annotations completed`) 86 | } 87 | } 88 | 89 | export async function cleanBuild() { 90 | const defold = config.defold 91 | 92 | if (!defold) { 93 | return wizard.suggestSetup(`Cleaning the build requires to setup ${config.extension.displayName} first`) 94 | } 95 | 96 | if (!await utils.isPathExists(config.paths.workspaceBuild)) { 97 | vscode.window.showInformationMessage(`There is no build folder to clean`) 98 | return 99 | } 100 | 101 | const isClean = await vscode.window.withProgress({ 102 | location: vscode.ProgressLocation.Notification, 103 | title: 'Cleaning the build folder...' 104 | }, async progress => { 105 | return await bob.cleanBuild(defold) 106 | }) 107 | 108 | if (isClean) { 109 | vscode.window.showInformationMessage(`Build cleaning completed`) 110 | } 111 | } 112 | 113 | export async function resolve() { 114 | const defold = config.defold 115 | 116 | if (!defold) { 117 | return wizard.suggestSetup(`Resolving dependencies requires to setup ${config.extension.displayName} first`) 118 | } 119 | 120 | const email = utils.settingsString(config.settingsKeys.dependenciesEmail) 121 | const auth = utils.settingsString(config.settingsKeys.dependenciesAuthToken) 122 | 123 | const isResolved = await vscode.window.withProgress({ 124 | location: vscode.ProgressLocation.Notification, 125 | title: 'Resolving dependencies...' 126 | }, async progress => { 127 | return await bob.resolve(defold, { 128 | email: email, 129 | auth: auth 130 | }) 131 | }) 132 | 133 | if (isResolved) { 134 | vscode.window.showInformationMessage(`Dependencies resolved`) 135 | } 136 | } 137 | 138 | export async function bundle() { 139 | const defold = config.defold 140 | 141 | if (!defold) { 142 | return wizard.suggestSetup(`Bundling requires to setup ${config.extension.displayName} first`) 143 | } 144 | 145 | const targets = await wizard.offerSelectBundleTargets() 146 | 147 | if (!targets || targets.length == 0) { 148 | log('Bundle is cancelled during the targets dialog') 149 | return 150 | } 151 | 152 | const options = await wizard.offerSelectBundleOptions() 153 | 154 | if (!options) { 155 | log('Bundle is cancelled during the options dialog') 156 | return 157 | } 158 | 159 | let bundlePath: string | undefined 160 | 161 | for (const target of targets) { 162 | const iosProvisioningProfileSettingsKey = options.isRelease ? config.settingsKeys.iosReleaseProvisioningProfile : config.settingsKeys.iosDebugProvisioningProfile 163 | const iosProvisioningProfile = utils.settingsString(iosProvisioningProfileSettingsKey) 164 | 165 | const iosIdentitySettingsKey = options.isRelease ? config.settingsKeys.iosReleaseIdentity : config.settingsKeys.iosDebugIdentity 166 | const iosIdentity = utils.settingsString(iosIdentitySettingsKey) 167 | 168 | const androidKeystore = options.isRelease ? utils.settingsString(config.settingsKeys.androidKeystore) : undefined 169 | const androidKeystorePass = options.isRelease ? utils.settingsString(config.settingsKeys.androidKeystorePass) : undefined 170 | const androidKeystoreAlias = options.isRelease ? utils.settingsString(config.settingsKeys.androidKeystoreAlias) : undefined 171 | 172 | log(`Bundling for ${target}`) 173 | 174 | bundlePath = await vscode.window.withProgress({ 175 | location: vscode.ProgressLocation.Notification, 176 | title: `Bundling for ${target}...` 177 | }, async progress => { 178 | return await bob.bundle(defold, { 179 | target: target, 180 | release: options.isRelease, 181 | textureCompression: options.textureCompression, 182 | buildReport: options.buildReport, 183 | debugSymbols: options.debugSymbols, 184 | liveUpdate: options.liveUpdate, 185 | iosMobileProvisioning: iosProvisioningProfile, 186 | iosIdentity: iosIdentity, 187 | androidKeystore: androidKeystore, 188 | androidKeystorePass: androidKeystorePass, 189 | androidKeystoreAlias: androidKeystoreAlias, 190 | androidBundleFormat: config.constants.androidBundleFormats, 191 | }) 192 | }) 193 | 194 | if (bundlePath) { 195 | const pathToOpen = bundlePath 196 | 197 | vscode.window.showInformationMessage( 198 | `Bundle for ${target} has been finished`, 199 | 'Open Bundle' 200 | ).then( shouldOpen => { 201 | if (shouldOpen) { 202 | vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(pathToOpen)) 203 | } 204 | }) 205 | } else { 206 | vscode.window.showWarningMessage(`Failed to bundle for ${target}. See Output for details.`) 207 | } 208 | } 209 | } 210 | 211 | export async function deploy() { 212 | const iosItem: vscode.QuickPickItem = { 213 | label: '$(device-mobile) iOS', 214 | detail: 'Deploy the .ipa file with the `ios-deploy -b` command', 215 | alwaysShow: true 216 | } 217 | 218 | const androidItem: vscode.QuickPickItem = { 219 | label: '$(device-mobile) Android', 220 | detail: 'Deploy the .apk file with the `adb install` command', 221 | alwaysShow: true 222 | } 223 | 224 | const targetItems = [ 225 | iosItem, 226 | androidItem 227 | ] 228 | 229 | const targetItem = await vscode.window.showQuickPick(targetItems, { 230 | canPickMany: false, 231 | title: 'Deploy Target', 232 | placeHolder: 'Select a target platform to deploy', 233 | ignoreFocusOut: false, 234 | }) 235 | 236 | if (!targetItem) { 237 | log('Deploy is cancelled during the target dialog') 238 | return 239 | } 240 | 241 | const target = targetItem == iosItem ? 'iOS' : 'Android' 242 | 243 | await vscode.window.withProgress({ 244 | location: vscode.ProgressLocation.Notification, 245 | title: `Deploying for ${targetItem.label}...` 246 | }, async progress => { 247 | return await deployer.deploy(target) 248 | }) 249 | } 250 | 251 | export async function build() { 252 | const defold = config.defold 253 | 254 | if (!defold) { 255 | return wizard.suggestSetup(`Building requires to setup ${config.extension.displayName} first`) 256 | } 257 | 258 | if (!await utils.isPathExists(config.paths.workspaceLibs)) { 259 | await resolve() 260 | } 261 | 262 | const isBuilded = await vscode.window.withProgress({ 263 | location: vscode.ProgressLocation.Notification, 264 | title: 'Building...' 265 | }, async progress => { 266 | return await bob.build(defold) 267 | }) 268 | 269 | if (!isBuilded) { 270 | vscode.window.showErrorMessage('Failed to build for running. See Output for details.') 271 | await utils.deleteFile(config.paths.workspaceBuildLauncher) 272 | return 273 | } 274 | 275 | const isPrepared = await vscode.window.withProgress({ 276 | location: vscode.ProgressLocation.Notification, 277 | title: 'Preparing to Launch...' 278 | }, async progress => { 279 | return await launcher.prepare(defold) 280 | }) 281 | 282 | if (!isPrepared) { 283 | vscode.window.showErrorMessage('Failed to prepare the launcher. See Output for details.') 284 | } 285 | } 286 | 287 | export async function openDefold() { 288 | const defold = config.defold 289 | 290 | if (!defold) { 291 | return wizard.suggestSetup(`Opening Defold requires to setup ${config.extension.displayName} first`) 292 | } 293 | 294 | await launcher.openDefold(defold) 295 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as utils from './utils' 13 | import log from './logger' 14 | import path = require('path') 15 | import fs = require('fs') 16 | const ini = require('ini') 17 | 18 | export let context: vscode.ExtensionContext 19 | export let workspaceFolder: vscode.WorkspaceFolder 20 | export let paths: PathsConfig 21 | export let defold: DefoldConfiguration | undefined 22 | 23 | export const lastVersionWithoutMigrationTracking = "2.0.5" 24 | 25 | export namespace extension { 26 | 27 | export let id: string 28 | export let version: string 29 | export let displayName: string 30 | export let commandPrefix: string 31 | export let taskPrefix: string 32 | 33 | } 34 | 35 | export const platforms = { 36 | windows: 'win32', 37 | macos: 'darwin', 38 | linux: 'linux' 39 | } 40 | 41 | export const defoldPathSuggestion = { 42 | [platforms.windows]: 'C:\\Program Files\\Defold', 43 | [platforms.macos]: '/Applications/Defold.app', 44 | [platforms.linux]: '/usr/bin/Defold' 45 | }[process.platform] 46 | 47 | 48 | export const defoldProcess = { 49 | [platforms.windows]: 'Defold.exe', 50 | [platforms.macos]: 'Defold', 51 | [platforms.linux]: 'Defold' 52 | }[process.platform] 53 | 54 | export const constants = { 55 | homedirShortcut: '~', 56 | gameProject: 'game.project', 57 | bobClass: 'com.dynamo.bob.Bob', 58 | sumnekoSettingsLibraryKey: 'Lua.workspace.library', 59 | androidBundleFormats: 'aab,apk', 60 | libsAutosyncTimeout: 5000 61 | } 62 | 63 | export const settingsKeys = { 64 | editorPath: 'defoldKit.general.editorPath', 65 | suggestSetup: 'defoldKit.general.suggestSetup', 66 | showBobOutput: 'defoldKit.general.showBobOutput', 67 | annotationsRepository: 'defoldKit.annotations.repository', 68 | annotationsAutoSyncDefold: 'defoldKit.annotations.autosync.defold', 69 | annotationsAutoSyncLibs: 'defoldKit.annotations.autosync.libs', 70 | dependenciesEmail: 'defoldKit.dependencies.email', 71 | dependenciesAuthToken: 'defoldKit.dependencies.authToken', 72 | iosDebugProvisioningProfile: 'defoldKit.bundle.ios.debug.provisioningProfile', 73 | iosDebugIdentity: 'defoldKit.bundle.ios.debug.identity', 74 | iosReleaseProvisioningProfile: 'defoldKit.bundle.ios.release.provisioningProfile', 75 | iosReleaseIdentity: 'defoldKit.bundle.ios.release.identity', 76 | androidKeystore: 'defoldKit.bundle.android.keystore', 77 | androidKeystorePass: 'defoldKit.bundle.android.keystorePass', 78 | androidKeystoreAlias: 'defoldKit.bundle.android.keystoreAlias' 79 | } 80 | 81 | export namespace launch { 82 | 83 | export const terminalName = 'Defold Engine' 84 | export const projectArg = path.join('build', 'default', 'game.projectc') 85 | 86 | export const configs = { 87 | [platforms.windows]: { 88 | buildPlatform: 'x86_64-win32', 89 | libexecBinPath: path.join('libexec', 'x86_64-win32'), 90 | executable: 'dmengine.exe', 91 | requiredFiles: [] 92 | }, 93 | [platforms.macos]: { 94 | buildPlatform: process.arch == 'arm64' ? 'arm64-osx' : 'x86_64-osx', 95 | libexecBinPath: path.join('libexec', process.arch == 'arm64' ? 'arm64-macos' : 'x86_64-macos'), 96 | executable: 'dmengine', 97 | requiredFiles: [] 98 | }, 99 | [platforms.linux]: { 100 | buildPlatform: 'x86_64-linux', 101 | libexecBinPath: path.join('libexec', 'x86_64-linux'), 102 | executable: 'dmengine', 103 | requiredFiles: [] 104 | } 105 | } 106 | } 107 | 108 | export const bundleTargets = { 109 | iOS: { 110 | label: '$(device-mobile) iOS', 111 | platform: 'arm64-ios', 112 | architectures: 'arm64-ios' 113 | }, 114 | Android: { 115 | label: '$(device-mobile) Android', 116 | platform: 'armv7-android', 117 | architectures: 'armv7-android,arm64-android' 118 | }, 119 | Windows: { 120 | label: '$(device-desktop) Windows', 121 | platform: 'x86_64-win32', 122 | architectures: 'x86_64-win32' 123 | }, 124 | macOS: { 125 | label: '$(device-desktop) macOS', 126 | platform: 'x86_64-macos', 127 | architectures: 'x86_64-macos,arm64-macos' 128 | }, 129 | Linux: { 130 | label: '$(device-desktop) Linux', 131 | platform: 'x86_64-linux', 132 | architectures: 'x86_64-linux' 133 | }, 134 | HTML5: { 135 | label: '$(globe) HTML5', 136 | platform: 'js-web', 137 | architectures: 'js-web,wasm-web' 138 | } 139 | } 140 | 141 | export namespace urls { 142 | 143 | const defaultRepositoryKey = 'astrochili/defold-annotations' 144 | 145 | const assets = { 146 | ['astrochili/defold-annotations']: 'releases/download/${tag}/defold_api_${tag}.zip', 147 | ['mikatuo/defold-lua-annotations']: 'releases/download/${tag}/defold-lua-${tag}.zip' 148 | } 149 | 150 | export function fallbackReleaseUrl(repositoryKey: string | undefined): string { 151 | const repository = repositoryKey ?? defaultRepositoryKey 152 | return `https://api.github.com/repos/${repository}/releases/latest` 153 | } 154 | 155 | export function annotationsAsset(tag: string, repositoryKey: string | undefined): string { 156 | const repository = repositoryKey ?? defaultRepositoryKey 157 | const assetTemplate = assets[repository as keyof typeof assets] ?? assets[defaultRepositoryKey] 158 | const asset = assetTemplate.replaceAll('${tag}', tag) 159 | 160 | return `https://github.com/${repository}/${asset}` 161 | } 162 | 163 | } 164 | 165 | export interface PathsConfig { 166 | resources: string, 167 | relativeDebuggerLua: string, 168 | relativeDebuggerScript: string, 169 | relativeBuildLauncher: string, 170 | workspaceStorage: string, 171 | globalStorage: string, 172 | defoldApi: string 173 | libsApi: string 174 | workspace: string, 175 | workspaceLaunch: string, 176 | workspaceRecommendations: string, 177 | workspaceBuild: string, 178 | workspaceBuildLauncher: string, 179 | workspaceLibs: string, 180 | workspaceGameProject: string 181 | } 182 | 183 | function makePathsConfig(globalStoragePath: string, workspaceStoragePath: string): PathsConfig { 184 | const workspace = workspaceFolder.uri.fsPath 185 | const resources = context.asAbsolutePath('resources') 186 | const relativeBuildLauncher = path.join('build', 'defoldkit') 187 | 188 | const config: PathsConfig = { 189 | resources: resources, 190 | relativeDebuggerLua: path.join('debugger', 'debugger.lua'), 191 | relativeDebuggerScript: path.join('debugger', 'debugger.script'), 192 | relativeBuildLauncher: relativeBuildLauncher, 193 | 194 | workspaceStorage: workspaceStoragePath, 195 | globalStorage: globalStoragePath, 196 | 197 | // Uses `defold_api` folder to avoid direct autocompletion in the require function 198 | defoldApi: path.join(globalStoragePath, 'defold_api'), 199 | // Doesn't use `libs_api` folder to have the same path in the settings 200 | libsApi: workspaceStoragePath, 201 | 202 | workspace: workspaceFolder.uri.fsPath, 203 | workspaceLaunch: path.join(workspace, '.vscode', 'launch.json'), 204 | workspaceRecommendations: path.join(workspace, '.vscode', 'extensions.json'), 205 | workspaceBuild: path.join(workspace, 'build'), 206 | workspaceBuildLauncher: path.join(workspace, relativeBuildLauncher), 207 | workspaceLibs: path.join(workspace, '.internal', 'lib'), 208 | workspaceGameProject: path.join(workspace, constants.gameProject), 209 | } 210 | 211 | return config 212 | } 213 | 214 | export interface DefoldConfiguration { 215 | version: string, 216 | editorPath: string, 217 | editorJar: string, 218 | javaBin: string, 219 | jarBin: string 220 | } 221 | 222 | async function makeDefoldConfig(defoldPath: string): Promise { 223 | const defoldResourcesPath = utils.isMac ? path.join(defoldPath, 'Contents', 'Resources') : defoldPath 224 | const defoldConfigPath = path.join(defoldResourcesPath, 'config') 225 | 226 | if (!await utils.isPathExists(defoldConfigPath)) { 227 | log(`Looks like a wrong path to the Defold Editor, the '${defoldResourcesPath}' file not found`) 228 | return 229 | } 230 | 231 | const rawConfig = await utils.readTextFile(defoldConfigPath) 232 | 233 | if (!rawConfig) { 234 | vscode.window.showErrorMessage(`Failed to read Defold Editor config file. See Output for details.`) 235 | return 236 | } 237 | 238 | const config = ini.parse(rawConfig) 239 | 240 | const jdkPath = path.join(defoldResourcesPath, config.launcher.jdk.replace( 241 | '\$\{bootstrap.resourcespath\}', 242 | config.bootstrap.resourcespath 243 | )) 244 | 245 | const javaBin = path.normalize(config.launcher.java.replace( 246 | '\$\{launcher.jdk\}', 247 | jdkPath 248 | )) 249 | 250 | const jarBin = javaBin.replace( 251 | `${path.sep}java`, 252 | `${path.sep}jar`, 253 | ) 254 | 255 | const editorJar = path.join(defoldResourcesPath, config.launcher.jar.replace( 256 | '\$\{bootstrap.resourcespath\}', 257 | config.bootstrap.resourcespath 258 | ).replace( 259 | '\$\{build.editor_sha1\}', 260 | config.build.editor_sha1 261 | )) 262 | 263 | return { 264 | version: config.build.version, 265 | editorPath: defoldPath, 266 | editorJar: editorJar, 267 | javaBin: javaBin, 268 | jarBin: jarBin 269 | } 270 | } 271 | 272 | export async function init( 273 | extensionContext: vscode.ExtensionContext, 274 | folder: vscode.WorkspaceFolder, 275 | workspaceStoragePath: string, 276 | globalStoragePath: string 277 | ) { 278 | context = extensionContext 279 | 280 | extension.id = context.extension.id 281 | extension.version = context.extension.packageJSON.version 282 | extension.displayName = context.extension.packageJSON.displayName 283 | extension.commandPrefix = context.extension.packageJSON.contributesPrefix 284 | extension.taskPrefix = context.extension.packageJSON.contributesPrefix 285 | 286 | workspaceFolder = folder 287 | paths = makePathsConfig(globalStoragePath, workspaceStoragePath) 288 | 289 | const defoldPath = utils.settingsString(settingsKeys.editorPath) 290 | 291 | if (defoldPath) { 292 | defold = await makeDefoldConfig(defoldPath) 293 | } else { 294 | defold = undefined 295 | log(`No path to Defold Editor found so no need to initialize Defold condiguration`) 296 | } 297 | 298 | log('Configuration' 299 | + `\n- config.workspace: '${paths.workspace}'` 300 | + `\n- config.resources: '${paths.resources}'` 301 | + `\n- config.workspaceStorage: '${paths.workspaceStorage}'` 302 | + `\n- config.globalStorage: '${paths.globalStorage}'` 303 | + `\n- config.defold: '${defold?.editorPath}'` 304 | ) 305 | } 306 | 307 | export async function updateDefoldPath(path: string): Promise { 308 | try { 309 | await vscode.workspace.getConfiguration().update(settingsKeys.editorPath, path, true) 310 | } catch (error) { 311 | vscode.window.showErrorMessage(`Failed to update Defold path in settings. See Output for details.`) 312 | log('Unexpected error during updating the Defold path in global settings') 313 | log(`${error}`, { openOutput: true }) 314 | } 315 | 316 | defold = await makeDefoldConfig(path) 317 | log(`The Defold Editor path is updated to '${path}'`) 318 | 319 | return defold 320 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/astrochili/defold-trenchbroom/assets/4752473/ab6e5282-dd37-4586-bcab-c1ac83cf1a9f) 2 | 3 | # Defold Kit 4 | 5 | [![Version](https://img.shields.io/visual-studio-marketplace/v/astronachos.defold)](https://marketplace.visualstudio.com/items?itemName=astronachos.defold) 6 | [![MIT License](https://img.shields.io/badge/License-MIT-blue)](https://github.com/astrochili/vscode-defold/blob/master/LICENSE) 7 | [![Website](https://img.shields.io/badge/website-gray.svg?&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxNiIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDE4IDE2Ij48Y2lyY2xlIGN4PSIzLjY2IiBjeT0iMTQuNzUiIHI9IjEuMjUiIGZpbGw9InVybCgjYSkiLz48Y2lyY2xlIGN4PSI4LjY2IiBjeT0iMTQuNzUiIHI9IjEuMjUiIGZpbGw9InVybCgjYikiLz48Y2lyY2xlIGN4PSIxMy42NSIgY3k9IjE0Ljc1IiByPSIxLjI1IiBmaWxsPSJ1cmwoI2MpIi8+PHBhdGggZmlsbD0idXJsKCNkKSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNNy42MyAxLjQ4Yy41LS43IDEuNTUtLjcgMi4wNSAwbDYuMjIgOC44MWMuNTguODMtLjAxIDEuOTctMS4wMyAxLjk3SDIuNDRhMS4yNSAxLjI1IDAgMCAxLTEuMDItMS45N2w2LjIxLTguODFaIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIyLjQxIiB4Mj0iMi40MSIgeTE9IjEzLjUiIHkyPSIxNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRDhENDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGOTU0MUYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjcuNDEiIHgyPSI3LjQxIiB5MT0iMTMuNSIgeTI9IjE2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iI0ZEOEQ0MiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y5NTQxRiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJjIiB4MT0iMTIuNCIgeDI9IjEyLjQiIHkxPSIxMy41IiB5Mj0iMTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjRkQ4RDQyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjk1NDFGIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImQiIHgxPSIuMDMiIHgyPSIuMDMiIHkxPSIuMDMiIHkyPSIxMi4yNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRkU2NUUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRkM4MzAiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4=)](https://astronachos.com/) 8 | [![Mastodon](https://img.shields.io/badge/mastodon-gray?&logo=mastodon)](https://mastodon.gamedev.place/@astronachos) 9 | [![Twitter](https://img.shields.io/badge/twitter-gray?&logo=twitter)](https://twitter.com/astronachos) 10 | [![Telegram](https://img.shields.io/badge/telegram-gray?&logo=telegram)](https://t.me/astronachos) 11 | [![Buy me a coffee](https://img.shields.io/badge/buy_me_a_coffee-gray?&logo=buy%20me%20a%20coffee)](https://buymeacoffee.com/astrochili) 12 | 13 | A toolkit for [Visual Studio Code](https://code.visualstudio.com/) to develop, build, launch, debug, bundle and deploy a game with [Defold](https://defold.com/). 14 | 15 | - [x] 💼 Installing recommended extensions 16 | - [x] 📝 Lua highlighting, autocompletion and linting 17 | - [x] 🛠️ Applying relevant settings to the workspace 18 | - [x] 📘 Lua annotations for Defold API 19 | - [x] 📚 Lua annotations for dependencies 20 | - [x] 🚀 Building and launching 21 | - [x] 🔎 Debugging with breakpoints 22 | - [x] 📦 Bundling for all the platforms 23 | - [x] 📲 Deploying to connected mobile devices 24 | 25 | You can not to use the Defold Editor at all if you are only working with code at the moment. 26 | 27 | 💬 [Discuss on the forum](https://forum.defold.com/t/defold-kit-visual-studio-code-extension/74119) 28 | 29 | ## Setup 30 | 31 | It's possible to install the extension from [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=astronachos.defold) or manually by downloading the `.vsix` archive from the [releases](https://github.com/astrochili/vscode-defold/releases) page. 32 | 33 | After installing the extension and opening a Defold project folder, you will be prompted to setup Defold Kit with a step-by-step dialogue. 34 | 35 | ![screenshot-suggest](https://github.com/astrochili/vscode-defold/assets/4752473/102e4069-0b8b-4e42-9195-f4839d22b794) 36 | 37 | If for some reason this doesn't happen, you can run the [Setup](#setup-1) command manually. 38 | 39 | ### Path to Defold 40 | 41 | ![screenshot-defold](https://github.com/astrochili/vscode-defold/assets/4752473/d0790cba-3c21-4f9b-bee3-2d2980e1ca3f) 42 | 43 | Defold Kit requires [Defold](https://defold.com) installed (surprise). 44 | 45 | Select the automatically suggested path if it exists, or select the Defold folder manually. 46 | 47 | ### Extensions 48 | 49 | ![screenshot-extensions](https://github.com/astrochili/vscode-defold/assets/4752473/4e049d1c-320d-4406-ac84-b1b2dda644a6) 50 | 51 | Select the extensions you want to install. 52 | 53 | The first two are highly recommended, the next three are optional: 54 | 55 | - [sumneko.lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) - Autocompletion, annotations, diagnostics and etc. 56 | - [tomblind.local-lua-debugger-vscode](https://marketplace.visualstudio.com/items?itemName=tomblind.local-lua-debugger-vscode) - Launching the game and debugging with breakpoints. 57 | - [thejustinwalsh.textproto-grammer](https://marketplace.visualstudio.com/items?itemName=thejustinwalsh.textproto-grammer) - Syntax highlighting for `.collection`, `.go` and other Protobuf files. 58 | - [slevesque.shader](https://marketplace.visualstudio.com/items?itemName=slevesque.shader) - GLSL support for `.vp` and `.fp` files. 59 | - [dtoplak.vscode-glsllint](https://marketplace.visualstudio.com/items?itemName=dtoplak.vscode-glsllint) - GLSL linting for `.vp` and `.fp` files. 60 | 61 | ### Workspace 62 | 63 | ![screenshot-settings](https://github.com/astrochili/vscode-defold/assets/4752473/521af9f5-ee6f-4c51-b024-dac825560e15) 64 | 65 | Some additional settings to apply to the workspace. 66 | 67 | #### Debugger Scripts 68 | 69 | > Displayed if the [`tomblind.local-lua-debugger-vscode`](#extensions) extension is installed. 70 | 71 | To debug the game with breakpoints, it's required to [start the debugger](#breakpoints) on the game side. These files allow you to do that. 72 | 73 | #### Launch Configuration 74 | 75 | > Displayed if the [`tomblind.local-lua-debugger-vscode`](#extensions) extension is installed. 76 | 77 | To launch the game from the **Run and Debug** panel, it's required to add the relevant configuration to the `.vscode/launch.json` file. The `Defold` configuration will be added. 78 | 79 | #### Workspace Settings 80 | 81 | These settings are recommended for Defold project workspace to make your development more comfortable. 82 | 83 | The settings will only be applied to the installed extensions. You can check them in the [settings.ts](src/data/settings.ts). 84 | 85 | #### Workspace Recommendations 86 | 87 | Adds Defold Kit to the `.vscode/extensions.json` file to appear in the recommended extensions to install for this workspace. 88 | 89 | ### Annotations Syncing 90 | 91 | ![screenshot-annotations](https://github.com/astrochili/vscode-defold/assets/4752473/88878bce-abb8-4dd2-bf76-46f7668e9129) 92 | 93 | > This step is skipping if the [`sumneko.lua`](#extensions) extension is not installed. 94 | 95 | Synchronise Lua annotations with the Defold Editor version and project dependencies. 96 | 97 | #### Defold API Annotations 98 | 99 | Fethes Defold API annotations from the repository according the [settings](#defoldkitannotationsrepository) and unpacks them to the Defold Kit shared storage. 100 | 101 | #### Dependencies Annotations 102 | 103 | Unpacks archives from the `.internal/libs` folder and copies `*.lua` files from libraries to the Defold Kit workspace storage. 104 | 105 | ## Opening Files from Defold 106 | 107 | ![screenshot-preferences](https://github.com/astrochili/vscode-defold/assets/4752473/d5107629-5f4c-4010-9851-002266fd5ff8) 108 | 109 | To open script files from the Defold Editor directly in Visual Studio Code, you must set the following settings by specifying the path to the executable file: 110 | 111 | - Windows (user installer): `C:\Users\%USERNAME%\AppData\Local\Programs\Microsoft VS Code\Code.exe` 112 | - Windows (system installer): `C:\Program Files\Microsoft VS Code\Code.exe` 113 | - macOS: `/Applications/Visual Studio Code.app/Contents/MacOS/Electron` 114 | - Linux: `/usr/bin/code` 115 | 116 | Set these parameters to open specific files and lines: 117 | 118 | - Open File: `. {file}` 119 | - Open File at Line: `. -g {file}:{line}` 120 | 121 | The `.` character here is required to open the entire workspace, not an individual file. 122 | 123 | ## Run and Debug 124 | 125 | ![screenshot-debugger](https://github.com/astrochili/vscode-defold/assets/4752473/9a3c2184-645a-49e9-ba72-1de02290d7a5) 126 | 127 | ### Launch 128 | 129 | To launch a game ensure that two these steps are done during [setting up](#setup) Defold Kit: 130 | 131 | - The [`local-lua-debugger-vscode`](#extensions) extension is installed. 132 | - [Launch configiuration](#launch-configuration) are added to the workspace. 133 | 134 | Ensure that the `Defold` configuration selected on the **Run and Debug** panel and launch it using the `F5` keyboard shortcut (default). 135 | 136 | ### Breakpoints 137 | 138 | To make the breakpoints work ensure that [Debugger scripts](#debugger-scripts) are added to the workspace. Then add the `debugger.script` component to your initial collection *or* add this code to your initial script: 139 | 140 | ```lua 141 | local debugger = require('debugger.debugger') 142 | debugger.start() 143 | ``` 144 | 145 | ## Commands 146 | 147 | ![screenshot-commands](https://github.com/astrochili/vscode-defold/assets/4752473/6fff2c8d-1b49-4843-b187-be8a22cf248c) 148 | 149 | Commands with the `Defold Kit` prefix are available in the Command Palette using the `[Ctrl/Cmd]-Shift-P` keyboard shortcut (default). 150 | 151 | ### Setup 152 | 153 | Starts the [setup dialogue](#path-to-defold). It's okay to run this command many times if you are not sure you are ready to turn on all the features at once. 154 | 155 | ### Sync API Annotations 156 | 157 | Opens the [Annotations Syncing](#annotations-syncing) dialogue. 158 | 159 | ### Clean API Annotations 160 | 161 | Deletes all the previously synced annotations from the global storage and workspace storage. 162 | 163 | ### Open Defold 164 | 165 | Opens the current project in the Defold Editor. 166 | 167 | On macOS, the window will be switched if Defold is already running. 168 | 169 | ### Clean Build 170 | 171 | Runs a [bob](https://defold.com/manuals/bob/) instance with the `distclean` argument to clean the build folder. 172 | 173 | ### Resolve Dependencies 174 | 175 | Runs a [bob](https://defold.com/manuals/bob/) instance with the `resolve` argument to resolve the project's dependencies. Then synchronises Lua annotations if the Lua Language Server is installed. 176 | 177 | ### Bundle 178 | 179 | Runs a [bob](https://defold.com/manuals/bob/) instance with the `resolve distclean build biundle` arguments, selected options and defined values form [settings](#settings). 180 | 181 | Executes for all selected target platforms one by one. When finished will prompt you to open the bundle folder. 182 | 183 | #### Platforms 184 | 185 | ![screenshot-platforms](https://github.com/astrochili/vscode-defold/assets/4752473/7ed5addc-98ee-4598-a620-c21964ef0e64) 186 | 187 | Select which target platforms you want to bundle your game. 188 | 189 | #### Options 190 | 191 | ![screenshot-bundle](https://github.com/astrochili/vscode-defold/assets/4752473/0640062a-6736-4c7f-b6a3-fc5456bc5244) 192 | 193 | - **Release** — Bundle a [Release](https://defold.com/manuals/bundling/#release-vs-debug) variant (otherwise bundle Debug variant). 194 | - **Texture Compression** — Enable texture compression as specified in [texture profiles](https://defold.com/manuals/texture-profiles/). 195 | - **Generate Debug Symbols** — Generate the [symbol file](https://defold.com/manuals/debugging-native-code/#symbolicate-a-callstack) (if applicable). 196 | - **Generate Build Report** — Generate the [build report](https://defold.com/manuals/profiling/#build-reports) file. 197 | - **Publish Live Update Content** — Publish [Live update](https://defold.com/manuals/live-update/) content. 198 | 199 | ### Deploy to Mobile 200 | 201 | ![screenshot-deploy](https://github.com/astrochili/vscode-defold/assets/4752473/3f0f1e87-127f-4d78-a702-102e8e29cdf9) 202 | 203 | Deploy to the connected mobile device with [ios-deploy](https://github.com/ios-control/ios-deploy) for iOS and [adb](https://developer.android.com/studio/command-line/adb) for Android. These tools must be installed and accessible via shell. 204 | 205 | ```bash 206 | # Will execute for iOS 207 | ios-deploy -b ${ipa_file} 208 | 209 | # Will execute for Android 210 | adb install ${apk_file} 211 | ``` 212 | 213 | The `*.ipa` or `*.apk` file is required in the corresponding bundle folder, so run the [Bundle](#bundle) command before deploying. 214 | 215 | ## Tasks 216 | 217 | ![screenshot-tasks](https://github.com/astrochili/vscode-defold/assets/4752473/d49eabfb-18f8-4643-a74a-c2741c4b1afa) 218 | 219 | Build tasks with the `Defold Kit` prefix are available using the `[Ctrl/Cmd]-Shift-B` keyboard shortcut (default). 220 | 221 | Tasks are aliases of some [commands](#commands) described above to have a quick access to them. 222 | 223 | - [Resolve Dependencies](#clean-build) 224 | - [Clean Build](#clean-build) 225 | - [Bundle](#bundle) 226 | - [Deploy to Mobile](#deploy-to-mobile) 227 | 228 | ## Settings 229 | 230 | #### defoldKit.general.editorPath 231 | 232 | The path to the Defold Editor folder. 233 | 234 | Running the [Setup](#setup-1) command is the preferred way to update this value, but you can edit it manually if you're sure of what you are doing. 235 | 236 | #### defoldKit.general.suggestSetup 237 | 238 | Suggest to setup Defold Kit if the `game.project` file is found in the current workspace. 239 | 240 | #### defoldKit.general.showBobOutput 241 | 242 | Open the Output panel during a [bob](https://defold.com/manuals/bob/) instance executing. 243 | 244 | #### defoldKit.annotations.repository 245 | 246 | Where to get Defold API annotations. Three options are currently available: 247 | 248 | - [astrochili/defold-annotations](https://github.com/astrochili/defold-annotations) 249 | - [mikatuo/defold-lua-annotations](https://github.com/mikatuo/defold-lua-annotations) 250 | 251 | #### defoldKit.annotations.autosync.defold 252 | 253 | Automatically synchronize annotations for Defold API with the Defold editor version when needed at extension startup. 254 | 255 | #### defoldKit.annotations.autosync.libs 256 | 257 | Automatically synchronize annotations for dependencies when changes are detected in the `.internal/libs` folder. 258 | 259 | #### defoldKit.dependencies.email 260 | 261 | User email to resolve dependencies. 262 | 263 | Adds the `--email ${email}` argument during [Resolve Dependencies](#resolve-dependencies) and [Bundle](#bundle) commands execution. 264 | 265 | #### defoldKit.dependencies.authToken 266 | 267 | Authentication token to resolve dependencies. 268 | 269 | Adds the `--auth ${authToken}` argument during [Resolve Dependencies](#resolve-dependencies) and [Bundle](#bundle) commands execution. 270 | 271 | #### defoldKit.bundle.ios.debug.provisioningProfile 272 | 273 | Path to the `*.mobileprovision profile` for **Debug** variant on **iOS**. 274 | 275 | Adds the `--mobileprovisioning ${provisioningProfile}` argument during [Bundle](#bundle) command execution. 276 | 277 | #### defoldKit.bundle.ios.debug.identity 278 | 279 | Code signing identity for the **Debug** variant on **iOS**. 280 | 281 | Adds the `--identity ${identity}` argument during [Bundle](#bundle) command execution. 282 | 283 | #### defoldKit.bundle.ios.release.provisioningProfile 284 | 285 | Path to the `*.mobileprovision profile` for **Release** variant on **iOS**. 286 | 287 | Adds the `--mobileprovisioning ${provisioningProfile}` argument during [Bundle](#bundle) command execution. 288 | 289 | #### defoldKit.bundle.ios.release.identity 290 | 291 | Code signing identity for the **Release** variant on **iOS**. 292 | 293 | Adds the `--identity ${identity}` argument during [Bundle](#bundle) command execution. 294 | 295 | #### defoldKit.bundle.android.keystore 296 | 297 | Path to the `*.keystore` file for **Android**. 298 | 299 | Adds the `--keystore ${keystore}` argument during [Bundle](#bundle) command execution. 300 | 301 | #### defoldKit.bundle.android.keystorePass 302 | 303 | Path to the `*.keystore.pass.txt` file for **Android**. 304 | 305 | Adds the `--keystore-pass ${keystorePass}` argument during [Bundle](#bundle) command execution. 306 | 307 | #### defoldKit.bundle.android.keystoreAlias 308 | 309 | Name of the alias from the [keystore](#defoldkitbundleandroidkeystore) for **Android**. 310 | 311 | Adds the `--keystore-alias ${keystoreAlias}` argument during [Bundle](#bundle) command execution. 312 | 313 | ## Compatibility 314 | 315 | It's possible to uncheck all the options during [Defold Kit setup](#setup) and still be able to sync annotations, bundle and deploy the game. 316 | 317 | To use your own annotations solution you can skip the [Annotations Syncing](#annotations-syncing) step or run the [Clean API Annotations](#clean-api-annotations) command. 318 | 319 | Due to the flexibility of Defold Kit, it can be used in combination with the [Defold Buddy](https://marketplace.visualstudio.com/items?itemName=mikatuo.vscode-defold-ide) extension, which adds additional sugars and features. 320 | 321 | ## Logs 322 | 323 | - The extension outputs logs to the `Default Kit` output channel. 324 | - A running game with a debugger outputs logs to the debug console. 325 | - A running game without debugger outputs logs to the `Defold Engine` terminal instance. 326 | 327 | ## Troubleshooting 328 | 329 | > Defold Kit doesn't see installed extensions and prompts me to install them again. 330 | 331 | Make sure that these extensions are activated. Defold Kit cannot distinguish a deactivated extension from a missing extension due to the lack of the corresponding Visual Studio Code API. 332 | 333 | > The game launched, but the breakpoints don't work. 334 | 335 | Make sure that you [started the debugger](#breakpoints) on the game side. 336 | 337 | > Undefined global `spine`, `imgui`, `iap` and similar warnings for native extensions. 338 | 339 | Many native extensions lack Lua annotations and have only `*.script_api` files ([#36](https://github.com/astrochili/vscode-defold/issues/36)). Lua Language Server works only with Lua annotations so you have two options: 340 | 341 | 1) Make PR to the native extension's repository to add the Lua annotations. 342 | 343 | 2) Or just add `spine` or other native extension's namespace to the ignore list in the file `.vscode/settings.json`: 344 | 345 | ``` 346 | "Lua.diagnostics.globals": [ 347 | ..., 348 | "spine" 349 | ], 350 | ``` 351 | 352 | > Build in VS Code is fine, but Defold Editor fails with the message `module 'debugger.debugger' not found`. 353 | 354 | This can happen if you call a method on the requested module in a single line. Watch [defold/defold/7963](https://github.com/defold/defold/issues/7963) for updates. 355 | 356 | ```lua 357 | -- ✅ Correct way to require in Defold 358 | local debugger = require('debugger.debugger') 359 | debugger.start() 360 | 361 | -- 🚫 Wrong way, will fail in Defold Editor 362 | require('debugger.debugger').start() 363 | ``` 364 | 365 | ## Limitations 366 | 367 | ### Change breakpoints at runtime 368 | 369 | Breakpoints can be set before launch and changed **on pauses only**. But there is a workaround, you can bind some input key to call `debugger.requestBreak()` and execution will pause on this line. 370 | 371 | Watch [tomblind/local-lua-debugger-vscode/#32](https://github.com/tomblind/local-lua-debugger-vscode/issues/32) and [local-lua-debugger-vscode/pull/67](https://github.com/tomblind/local-lua-debugger-vscode/pull/67) for updates. 372 | 373 | ### Debug on mobile devices 374 | 375 | [Local Lua Debugger](https://github.com/tomblind/local-lua-debugger-vscode/) is a *local* debugger. So you can't debug the game on the device by this way. 376 | -------------------------------------------------------------------------------- /src/annotations.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as os from 'os' 13 | import * as crypto from 'crypto' 14 | import * as config from './config' 15 | import * as utils from './utils' 16 | import * as momento from './momento' 17 | import * as extensions from './data/extensions' 18 | import log from './logger' 19 | import axios from 'axios' 20 | import path = require('path') 21 | import Zip = require('adm-zip') 22 | const ini = require('ini') 23 | 24 | let libsFileSystemWatcher: vscode.FileSystemWatcher 25 | let libsAnnotationsSyncTimeout: NodeJS.Timeout | undefined 26 | 27 | interface LatestReleaseResponse { 28 | tag_name: string 29 | } 30 | 31 | async function startLibsAnnotationsSyncTimer() { 32 | clearTimeout(libsAnnotationsSyncTimeout) 33 | 34 | if (!utils.settingsBoolean(config.settingsKeys.annotationsAutoSyncLibs)) { 35 | return 36 | } 37 | 38 | libsAnnotationsSyncTimeout = setTimeout(async () => { 39 | log("Enougth time has passed since the last dependency update. Time to sync annotations.") 40 | 41 | await vscode.window.withProgress({ 42 | location: vscode.ProgressLocation.Notification, 43 | title: 'Syncing annotations' 44 | }, async progress => { 45 | progress.report({ message: 'Dependencies...' }) 46 | await syncDependenciesAnnotations() 47 | }) 48 | }, config.constants.libsAutosyncTimeout); 49 | } 50 | 51 | async function fetchLatestRelease(repositoryKey: string | undefined): Promise { 52 | const fallbackReleaseUrl = config.urls.fallbackReleaseUrl(repositoryKey) 53 | 54 | try { 55 | log(`Network request GET: '${fallbackReleaseUrl}'`) 56 | const response = await axios.get(fallbackReleaseUrl) 57 | log(`Network response OK'`) 58 | return response.data 59 | } catch (error) { 60 | log(`Network request FAILED'`) 61 | log(`'${error}'`) 62 | } 63 | } 64 | 65 | async function fetchSpecificAsset(tag: string, repositoryKey: string | undefined): Promise { 66 | const annotationsUrl = config.urls.annotationsAsset(tag, repositoryKey) 67 | 68 | try { 69 | log(`Network request GET: '${annotationsUrl}'`) 70 | const response = await axios.get(annotationsUrl, { responseType: 'arraybuffer' }) 71 | log(`Network response OK'`) 72 | return response.data 73 | } catch (error) { 74 | log(`Network request FAILED'`) 75 | log(`'${error}'`) 76 | } 77 | } 78 | 79 | async function fetchDefoldAnnotations(defoldVersion: string): Promise { 80 | log(`Starting fetch Defold annotations`) 81 | 82 | if (await utils.isPathExists(config.paths.globalStorage)) { 83 | log(`Global storage folder exists, no need to create`) 84 | } else { 85 | log(`Creating the global storage: ${config.paths.globalStorage}`) 86 | if (!await utils.createDirectory(config.paths.globalStorage)) { 87 | vscode.window.showErrorMessage(`Failed to create global storage folder. See Output for details.`) 88 | return 89 | } 90 | } 91 | 92 | log(`Defold version: ${config.defold?.version}`) 93 | 94 | const repositoryKey = utils.settingsString(config.settingsKeys.annotationsRepository) 95 | log(`Defold annotations repository: ${repositoryKey}`) 96 | 97 | const annotationsUrl = config.urls.annotationsAsset(defoldVersion, repositoryKey) 98 | log(`Direct annotations url: ${annotationsUrl}`) 99 | 100 | let annotationsVersion = defoldVersion 101 | let data = await fetchSpecificAsset(annotationsVersion, repositoryKey) 102 | 103 | if (!data) { 104 | log(`Failed to fetch the specific release`) 105 | log(`Let's try to fetch the latest available annotations`) 106 | const latestRelease = await fetchLatestRelease(repositoryKey) 107 | 108 | if (latestRelease) { 109 | annotationsVersion = latestRelease.tag_name 110 | data = await fetchSpecificAsset(annotationsVersion, repositoryKey) 111 | } 112 | } 113 | 114 | if (!data) { 115 | vscode.window.showErrorMessage(`Can't fetch Defold annotations. See Output for details.`); 116 | return 117 | } 118 | 119 | const apiPath = config.paths.defoldApi 120 | 121 | log(`Cleaning directory: ${apiPath}`) 122 | const isClean = await utils.deleteFile(apiPath) && await utils.createDirectory(apiPath) 123 | 124 | if (!isClean) { 125 | vscode.window.showErrorMessage(`Failed to clean the libraries api folder. See Output for details.`) 126 | return 127 | } 128 | 129 | const outputPath = path.join(apiPath, 'annotations.zip') 130 | 131 | log(`Writing response data to '${outputPath}'`) 132 | const isWriten = await utils.writeDataFile(outputPath, data) 133 | 134 | if (!isWriten) { 135 | vscode.window.showErrorMessage(`Can't fetch Defold annotations. See Output for details.`); 136 | return 137 | } 138 | 139 | try { 140 | log(`Initializing Zip instance for '${outputPath}'`) 141 | const zip = new Zip(outputPath) 142 | const entries = zip.getEntries() 143 | 144 | log(`Extracting lua files from '${outputPath}' to '${apiPath}'`) 145 | for (const entry of entries) { 146 | if (entry.name.endsWith('.lua')) { 147 | zip.extractEntryTo(entry, apiPath, false, true) 148 | } 149 | } 150 | } catch (error) { 151 | vscode.window.showErrorMessage(`Can't fetch Defold annotations. See Output for details.`); 152 | log(`Failed to unzip '${outputPath}' to '${apiPath}'`) 153 | log(`${error}`) 154 | } 155 | 156 | log(`Deleting '${outputPath}'`) 157 | await utils.deleteFile(outputPath) 158 | 159 | await momento.setAnnotationsVersion(annotationsVersion) 160 | 161 | return config.paths.globalStorage 162 | } 163 | 164 | async function unpackDependenciesAnnotations(): Promise { 165 | const libPath = config.paths.workspaceLibs 166 | 167 | if (!await utils.isPathExists(libPath)) { 168 | log(`Dependencies folder '${libPath}' not found, unpacking extensions skipped`) 169 | return 170 | } 171 | 172 | if (await utils.isPathExists(config.paths.workspaceStorage)) { 173 | log(`Workspace storage folder exists, no need to create`) 174 | } else { 175 | log(`Creating the workspace storage: ${config.paths.workspaceStorage}`) 176 | if (!await utils.createDirectory(config.paths.workspaceStorage)) { 177 | vscode.window.showErrorMessage(`Failed to create workspace storage folder. See Output for details.`) 178 | return 179 | } 180 | } 181 | 182 | const apiPath = config.paths.libsApi 183 | 184 | log(`Cleaning directory: ${apiPath}`) 185 | const isClean = await utils.deleteFile(apiPath) && await utils.createDirectory(apiPath) 186 | 187 | if (!isClean) { 188 | vscode.window.showErrorMessage(`Failed to clean the libraries api folder. See Output for details.`) 189 | return 190 | } 191 | 192 | let projectText 193 | log(`Reading list of dependencies in game project.`) 194 | 195 | if (await utils.isPathExists(config.paths.workspaceGameProject)) { 196 | projectText = await utils.readTextFile(config.paths.workspaceGameProject) 197 | } else { 198 | log(`The '${config.paths.workspaceGameProject}' file is not found.`) 199 | } 200 | 201 | if (!projectText) { 202 | vscode.window.showErrorMessage(`Failed to read the '${config.paths.workspaceGameProject}' file. See Output for details.`) 203 | return 204 | } 205 | 206 | log(`Parsing game project dependencies...`) 207 | let dependencyHashes: string[] = [] 208 | const projectLines = projectText.split('\n') 209 | const dependencyLines = projectLines.filter(line => line.startsWith('dependencies#')) 210 | 211 | for (const dependencyLine of dependencyLines) { 212 | const regex = /\s*[^=]+\s*=\s*(.*)/ 213 | const match = dependencyLine.match(regex) 214 | 215 | if (!match) { 216 | log(`Failed to parse dependency line '${dependencyLine}'`) 217 | continue 218 | } 219 | 220 | const sha = crypto.createHash(`sha1`) 221 | const dependencyUrl = match[1] 222 | 223 | try { 224 | sha.update(dependencyUrl) 225 | const dependencyHash = sha.digest(`hex`) 226 | dependencyHashes.push(dependencyHash) 227 | } catch(error) { 228 | log(`Failed to generate sha1 hash for dependency url ${dependencyUrl}`) 229 | continue 230 | } 231 | } 232 | 233 | log(`Reading extensions folder: ${libPath}`) 234 | let files = await utils.readDirectory(libPath) 235 | 236 | if (files == undefined) { 237 | vscode.window.showErrorMessage(`Failed to read the '.internal' folder. See Output for details.`) 238 | return 239 | } 240 | 241 | let libsFolderFilenames = new Array 242 | 243 | log(`Filtering *.zip files according project dependencies...`) 244 | files = files.filter(file => { 245 | const filename = file[0] 246 | 247 | if (!filename.endsWith('.zip')) { 248 | return false 249 | } 250 | 251 | libsFolderFilenames.push(filename) 252 | 253 | for (const dependenciesHash of dependencyHashes) { 254 | if (filename.startsWith(dependenciesHash)) { 255 | return true 256 | } 257 | } 258 | 259 | log(`File '${filename}' skipped.`) 260 | return false 261 | }) 262 | 263 | for (const file of files) { 264 | const filePath = path.join(libPath, file[0]) 265 | 266 | let zip: Zip | undefined 267 | let zipEntries: Zip.IZipEntry[] | undefined 268 | 269 | try { 270 | log(`Reading extension archive: ${filePath}`) 271 | zip = new Zip(filePath) 272 | zipEntries = zip.getEntries() 273 | } catch (error) { 274 | vscode.window.showWarningMessage(`Failed to read the '${file[0]}' archive. See Output for details.`) 275 | return 276 | } 277 | 278 | let libraryDirs = new Array 279 | let projectPath: string | undefined 280 | 281 | for (const zipEntry of zipEntries) { 282 | const internalPath = zipEntry.entryName 283 | 284 | if (internalPath.endsWith(config.constants.gameProject)) { 285 | log(`Found the project file: ${internalPath}`) 286 | 287 | const projectDir = path.dirname(internalPath) 288 | log(`The library project directory is: ${projectDir}`) 289 | projectPath = projectDir 290 | 291 | try { 292 | log(`Reading as text: ${internalPath}`) 293 | const projectContent = zip.readAsText(internalPath) 294 | 295 | log(`Parsing as INI...`) 296 | const project = ini.parse(projectContent) 297 | 298 | log(`Parsing library property...`) 299 | const includeDirs = project.library.include_dirs as string 300 | 301 | libraryDirs = includeDirs.split(',').map(directory => { 302 | return path.join(projectDir, directory.trim()) 303 | }) 304 | } catch (error) { 305 | log(`Failed to parse library dirs in the '${internalPath}' file`) 306 | log(`${error}`) 307 | } 308 | 309 | break 310 | } 311 | } 312 | 313 | if (!projectPath) { 314 | log(`The '${config.constants.gameProject}' file not found inside the archive ${filePath}`) 315 | return 316 | } 317 | 318 | log(`Unpacking from '${file[0]}' to '${path.join(apiPath, projectPath)}`) 319 | 320 | for (const zipEntry of zipEntries) { 321 | const internalPath = zipEntry.entryName.replaceAll('/', path.sep) 322 | 323 | for (const libraryDir of libraryDirs) { 324 | if (internalPath.startsWith(libraryDir) && internalPath.endsWith('.lua')) { 325 | try { 326 | let internalPathParts = internalPath.split(path.sep) 327 | internalPathParts = internalPathParts.splice(1, internalPathParts.length - 2) 328 | 329 | let targetExtractPath = path.join(...internalPathParts) 330 | targetExtractPath = path.join(apiPath, targetExtractPath) 331 | 332 | zip.extractEntryTo(zipEntry, targetExtractPath, false, true) 333 | } catch (error) { 334 | log(`Failed to unzip '${internalPath}' from '${file[0]}' to '${apiPath}'`) 335 | log(`${error}`) 336 | } 337 | } 338 | } 339 | } 340 | } 341 | 342 | const libsFolderHash = utils.hash(libsFolderFilenames.sort().join()) 343 | await momento.setLibsFolderHash(libsFolderHash) 344 | 345 | return config.paths.workspaceStorage 346 | } 347 | 348 | async function addToWorkspaceSettings(annotationsPath: string): Promise { 349 | log(`Updating annotations paths in workspace settings: '${annotationsPath}'`) 350 | 351 | if (!vscode.extensions.getExtension(extensions.ids.luaLanguageServer)) { 352 | log(`No need to update annotations paths because there is no '${extensions.ids.luaLanguageServer}' extensions`) 353 | return true 354 | } 355 | 356 | const configuration = vscode.workspace.getConfiguration(undefined, config.workspaceFolder) 357 | const libraryKey = config.constants.sumnekoSettingsLibraryKey 358 | let libraries = configuration.get(libraryKey) ?? [] 359 | 360 | const homeDir = vscode.Uri.file(os.homedir()).fsPath 361 | const safeAnnotationsPath = annotationsPath.replace(homeDir, config.constants.homedirShortcut) 362 | 363 | if (libraries.includes(safeAnnotationsPath)) { 364 | log(`The '${libraryKey}' already has this annotations path, no need to touch`) 365 | return true 366 | } else { 367 | log(`Adding '${safeAnnotationsPath}' to '${libraryKey}'`) 368 | libraries.push(safeAnnotationsPath) 369 | 370 | try { 371 | await configuration.update(libraryKey, libraries); 372 | return true 373 | } catch (error) { 374 | vscode.window.showErrorMessage(`Failed to add the annotations path to workspace settings. See Output for details.`) 375 | log(`Unexpected error during adding the annotations path to workspace settings`) 376 | log(`${error}`, { openOutput: true }) 377 | return false 378 | } 379 | } 380 | } 381 | 382 | async function removeFromWorkspaceSettings(annotationsPath: string): Promise { 383 | log(`Removing annotations paths in workspace settings: '${annotationsPath}'`) 384 | 385 | const configuration = vscode.workspace.getConfiguration(undefined, config.workspaceFolder) 386 | const libraryKey = config.constants.sumnekoSettingsLibraryKey 387 | let libraries = configuration.get(libraryKey) 388 | 389 | if (!libraries) { 390 | return true 391 | } 392 | 393 | const safeAnnotationsPath = annotationsPath.replace(os.homedir(), config.constants.homedirShortcut) 394 | 395 | if (!libraries.includes(safeAnnotationsPath)) { 396 | log(`The '${libraryKey}' doesn't have this annotations path, nothing to remove`) 397 | return true 398 | } else { 399 | log(`Removing '${safeAnnotationsPath}' from '${libraryKey}'`) 400 | libraries = libraries.filter(path => { return path != safeAnnotationsPath}) 401 | 402 | try { 403 | await configuration.update(libraryKey, libraries); 404 | return true 405 | } catch (error) { 406 | vscode.window.showErrorMessage(`Failed to remove the annotations path from workspace settings. See Output for details.`) 407 | log(`Unexpected error during removing the annotations path from workspace settings`) 408 | log(`${error}`, { openOutput: true }) 409 | return false 410 | } 411 | } 412 | } 413 | 414 | export async function startWatchingLibsToSyncAnnotations() { 415 | libsFileSystemWatcher = vscode.workspace.createFileSystemWatcher(path.join(config.paths.workspaceLibs, '*.zip')) 416 | 417 | libsFileSystemWatcher.onDidCreate(uri => { 418 | log(`Watched new dependency: ${uri.path}`) 419 | startLibsAnnotationsSyncTimer() 420 | }) 421 | 422 | libsFileSystemWatcher.onDidChange(uri => { 423 | log(`Watched dependency update: ${uri.path}`) 424 | startLibsAnnotationsSyncTimer() 425 | }) 426 | 427 | libsFileSystemWatcher.onDidDelete(uri => { 428 | log(`Watched dependency deletion: ${uri.path}`) 429 | startLibsAnnotationsSyncTimer() 430 | }) 431 | 432 | // TODO: Don't repeat yourself. It requires refactoring. 433 | const libPath = config.paths.workspaceLibs 434 | 435 | if (!await utils.isPathExists(libPath)) { 436 | log(`Dependencies folder '${libPath}' not found, skip comparing the folder hash`) 437 | return 438 | } 439 | 440 | log(`Reading extensions folder: ${libPath}`) 441 | let files = await utils.readDirectory(libPath) 442 | 443 | if (files == undefined) { 444 | vscode.window.showErrorMessage(`Failed to read the '.internal' folder. See Output for details.`) 445 | return 446 | } 447 | 448 | let libsFolderFilenames = new Array 449 | 450 | files.forEach(file => { 451 | const filename = file[0] 452 | 453 | if (filename.endsWith('.zip')) { 454 | libsFolderFilenames.push(filename) 455 | } 456 | }) 457 | 458 | const libsFolderHash = utils.hash(libsFolderFilenames.sort().join()) 459 | if (libsFolderHash != momento.getLibsFolderHash()) { 460 | startLibsAnnotationsSyncTimer() 461 | } 462 | } 463 | 464 | export async function syncDefoldAnnotations(defoldVersion: string): Promise { 465 | log('Syncing Defold API annotations') 466 | const defoldAnnotationsPath = await fetchDefoldAnnotations(defoldVersion) 467 | 468 | if (defoldAnnotationsPath) { 469 | return await addToWorkspaceSettings(defoldAnnotationsPath) 470 | } else { 471 | return false 472 | } 473 | } 474 | 475 | export async function syncDependenciesAnnotations(): Promise { 476 | log('Syncing dependencies annotations') 477 | const dependenciesAnnotationsPath = await unpackDependenciesAnnotations() 478 | 479 | if (dependenciesAnnotationsPath) { 480 | return await addToWorkspaceSettings(dependenciesAnnotationsPath) 481 | } else { 482 | return false 483 | } 484 | } 485 | 486 | export async function cleanAnnotations(): Promise { 487 | log(`Cleaning directory: ${config.paths.defoldApi}`) 488 | if (!await utils.deleteFile(config.paths.defoldApi)) { 489 | vscode.window.showErrorMessage(`Failed to clean the Defold annotations folder. See Output for details.`) 490 | return false 491 | } 492 | 493 | log(`Removing from the Defold annotations path workspace settings`) 494 | if (!await removeFromWorkspaceSettings(config.paths.globalStorage)) { 495 | vscode.window.showErrorMessage(`Failed to remove the Defold annotations path from the workspace settings. See Output for details.`) 496 | return false 497 | } 498 | 499 | log(`Cleaning directory: ${config.paths.libsApi}`) 500 | if (!await utils.deleteFile(config.paths.libsApi)) { 501 | vscode.window.showErrorMessage(`Failed to clean the dependencies annotations folder. See Output for details.`) 502 | return false 503 | } 504 | 505 | log(`Removing from the dependencies annotations path workspace settings`) 506 | if (!await removeFromWorkspaceSettings(config.paths.workspaceStorage)) { 507 | vscode.window.showErrorMessage(`Failed to remove the dependencies annotations path from the workspace settings. See Output for details.`) 508 | return false 509 | } 510 | 511 | return true 512 | } -------------------------------------------------------------------------------- /src/wizard.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Defold Kit 3 | * https://github.com/astrochili/vscode-defold 4 | * Copyright (c) 2023 Roman Silin 5 | * MIT license 6 | * 7 | * The Defold name is a registered trademark of the Defold Foundation. 8 | * https://defold.com 9 | */ 10 | 11 | import * as vscode from 'vscode' 12 | import * as JSON5 from 'json5' 13 | import * as config from './config' 14 | import * as momento from './momento' 15 | import * as utils from './utils' 16 | import * as annotations from './annotations' 17 | import * as extensions from './data/extensions' 18 | import * as debuggers from './data/debuggers' 19 | import log from './logger' 20 | import path = require('path') 21 | 22 | const encoder = new TextEncoder() 23 | 24 | async function copyDebuggerResources() { 25 | const resourcesDebuggerLua = path.join(config.paths.resources, config.paths.relativeDebuggerLua) 26 | const workspaceDebuggerLua = path.join(config.paths.workspace, config.paths.relativeDebuggerLua) 27 | 28 | if (await utils.isPathExists(workspaceDebuggerLua)) { 29 | log(`No need to copy '${config.paths.relativeDebuggerLua}', already exists`) 30 | } else { 31 | log(`Copying from '${resourcesDebuggerLua}' to '${workspaceDebuggerLua}'`) 32 | 33 | if (!await utils.copy(resourcesDebuggerLua, workspaceDebuggerLua)) { 34 | vscode.window.showWarningMessage(`Failed to copy '${config.paths.relativeDebuggerLua}' to the workspace. See Output for details.`) 35 | log(`Failed to copy '${config.paths.relativeDebuggerLua}' to the workspace'`) 36 | } 37 | } 38 | 39 | const resourcesDebuggerScript = path.join(config.paths.resources, config.paths.relativeDebuggerScript) 40 | const workspaceDebuggerScript = path.join(config.paths.workspace, config.paths.relativeDebuggerScript) 41 | 42 | if (await utils.isPathExists(workspaceDebuggerScript)) { 43 | log(`No need to copy '${config.paths.relativeDebuggerScript}', already exists`) 44 | } else { 45 | log(`Copying from '${resourcesDebuggerScript}' to '${workspaceDebuggerScript}'`) 46 | 47 | if (!await utils.copy(resourcesDebuggerScript, workspaceDebuggerScript)) { 48 | vscode.window.showWarningMessage(`Failed to copy '${config.paths.relativeDebuggerScript}' to the workspace. See Output for details.`) 49 | log(`Failed to copy '${config.paths.relativeDebuggerScript}' to the workspace'`) 50 | } 51 | } 52 | } 53 | 54 | async function applyDebugConfiguration(configuration: vscode.DebugConfiguration) { 55 | const launchSettings = vscode.workspace.getConfiguration('launch') 56 | let configurations = launchSettings.get('configurations') as vscode.DebugConfiguration[] 57 | 58 | if (configurations.find(existingConfiguration => { 59 | return existingConfiguration.name == configuration.name && existingConfiguration.type == configuration.type 60 | })) { 61 | log(`Launch configuration '${configuration.name}' with type '${configuration.type}' already exists, adding skipped.`) 62 | return 63 | } 64 | 65 | configurations.push(configuration) 66 | 67 | await launchSettings.update('configurations', configurations) 68 | } 69 | 70 | async function applyDebugConfigurations(extensionIds: string[]) { 71 | const debuggerIds = Object.keys(debuggers.recommended).filter(extensionId => { 72 | return extensionIds.includes(extensionId) || vscode.extensions.getExtension(extensionId) 73 | }) 74 | 75 | for (const extensionId of debuggerIds) { 76 | const configuration = debuggers.recommended[extensionId as keyof typeof debuggers.recommended] 77 | 78 | if (configuration) { 79 | log(`Found debug configuration for extension: '${extensionId}'`) 80 | 81 | try { 82 | await applyDebugConfiguration(configuration) 83 | log(`Applied debug configuration for extension: '${extensionId}'`) 84 | } catch (error) { 85 | vscode.window.showWarningMessage(`Failed to apply debug configuration for extension ${extensionId}`) 86 | log(`Failed to apply debug configuration for extension '${extensionId}'`) 87 | log(`${error}`) 88 | } 89 | } 90 | } 91 | } 92 | 93 | async function updateConfiguration( 94 | configuration: vscode.WorkspaceConfiguration, 95 | key: string, 96 | value: any, 97 | configurationTarget?: vscode.ConfigurationTarget | boolean | null, 98 | overrideInLanguage?: boolean 99 | ) { 100 | try { 101 | await configuration.update(key, value, configurationTarget, overrideInLanguage) 102 | } catch (error) { 103 | log(`Failed update configuration array at key '${key}' to value '${value}'`) 104 | log(`${error}`) 105 | } 106 | } 107 | 108 | async function applyExtensionSettings(settings: any) { 109 | const configuration = vscode.workspace.getConfiguration(undefined, 110 | config.workspaceFolder 111 | ) 112 | 113 | const luaConfiguration = vscode.workspace.getConfiguration(undefined, { 114 | uri: config.workspaceFolder.uri, 115 | languageId: 'lua' 116 | }) 117 | 118 | for (const key in settings) { 119 | const value = settings[key] 120 | 121 | if (Array.isArray(value)) { 122 | const array = configuration.get(key) ?? [] 123 | 124 | for (const item of value) { 125 | if (!array.includes(item)) { 126 | array.push(item) 127 | } 128 | } 129 | 130 | await updateConfiguration(configuration, key, array) 131 | } else if (typeof(value) == 'object') { 132 | if (key == '[lua]') { 133 | for (const key in value) { 134 | await updateConfiguration(luaConfiguration, key, value[key], undefined, true) 135 | } 136 | } else { 137 | let object = configuration.get(key) ?? {} 138 | object = Object.assign({}, object, value) 139 | await updateConfiguration(configuration, key, object) 140 | } 141 | } else { 142 | await updateConfiguration(configuration, key, value) 143 | } 144 | } 145 | } 146 | 147 | async function applyExtensionsSettings(extensionIds: string[]) { 148 | const settings = require('./data/settings') 149 | 150 | let settingsIds = Object.keys(extensions.recommended).filter(extensionId => { 151 | return extensionIds.includes(extensionId) || vscode.extensions.getExtension(extensionId) 152 | }) 153 | 154 | settingsIds.push(config.extension.id) 155 | 156 | for (const extensionId of settingsIds) { 157 | const extensionSettings = settings.recommended[extensionId as keyof typeof settings.recommended] 158 | 159 | if (extensionSettings) { 160 | log(`Found settings for extension: '${extensionId}'`) 161 | 162 | try { 163 | await applyExtensionSettings(extensionSettings) 164 | log(`Applied settings for extension: '${extensionId}'`) 165 | } catch (error) { 166 | vscode.window.showWarningMessage(`Failed to apply settings for extension ${extensionId}`) 167 | log(`Failed to apply settings for extension '${extensionId}'`) 168 | log(`${error}`) 169 | } 170 | } 171 | } 172 | } 173 | 174 | async function applyWorkspaceRecommendations() { 175 | let json, rawText 176 | 177 | if (await utils.isPathExists(config.paths.workspaceRecommendations)) { 178 | rawText = await utils.readTextFile(config.paths.workspaceRecommendations) 179 | } else { 180 | log(`The '.vscode/extensions.json' file is not found, it will be created.`) 181 | } 182 | 183 | if (rawText) { 184 | try { 185 | log(`Parsing JSON from text`) 186 | json = JSON5.parse(rawText) 187 | } catch (error) { 188 | vscode.window.showErrorMessage(`Failed to parse the '.vscode/extensions.json' file. Does it have valid JSON?`) 189 | log(`Failed to convert the '.vscode/extensions.json' file data to JSON`) 190 | log(`${error}`) 191 | return 192 | } 193 | } else { 194 | json = {} 195 | } 196 | 197 | const recommendations: string[] = json.recommendations as string[] || [] 198 | 199 | if (recommendations.includes(config.extension.id)) { 200 | log(`The '.vscode/extensions.json' file already has the '${config.extension.id}' recommendation`) 201 | return 202 | } 203 | 204 | recommendations.push(config.extension.id) 205 | json.recommendations = recommendations 206 | 207 | try { 208 | log(`Stringifying JSON to text `) 209 | rawText = JSON.stringify(json, null, 4) 210 | } catch (error) { 211 | vscode.window.showErrorMessage(`Failed to update workspace recommendations. See Output for details.`) 212 | log(`Failed to convert JSON to the file data`) 213 | log(`${error}`) 214 | return 215 | } 216 | 217 | log(`Writing the '${config.extension.id}' recommendation to the '.vscode/extensions.json' file`) 218 | const isWritten = await utils.writeTextFile(config.paths.workspaceRecommendations, rawText) 219 | 220 | if (!isWritten) { 221 | vscode.window.showErrorMessage(`Failed to update workspace recommendations. See Output for details.`) 222 | } 223 | } 224 | 225 | export async function suggestSetup(text: string, soft?: boolean) { 226 | log(`Suggest to setup ${config.extension.displayName}`) 227 | 228 | const setupButton = `Setup ${config.extension.displayName}` 229 | const dontAskButton = `Don't ask anymore` 230 | 231 | const answer = await ( 232 | soft ? 233 | vscode.window.showInformationMessage(text, setupButton, dontAskButton) : 234 | vscode.window.showInformationMessage(text, setupButton) 235 | ) 236 | 237 | switch (answer) { 238 | case setupButton: 239 | vscode.commands.executeCommand(`${config.extension.commandPrefix}.setup`) 240 | break 241 | case dontAskButton: 242 | await config.context.workspaceState.update(momento.keys.dontSuggestSetup, true) 243 | break 244 | 245 | default: 246 | break 247 | } 248 | } 249 | 250 | export async function suggestSetupIfApplicable(): Promise { 251 | if (!utils.settingsBoolean(config.settingsKeys.suggestSetup)) { 252 | log(`Setup suggestion is turned off in settings`) 253 | return false 254 | } 255 | 256 | if (await config.context.workspaceState.get(momento.keys.dontSuggestSetup)) { 257 | log(`The user has already asked not to suggest setup`) 258 | return false 259 | } 260 | 261 | const isDefoldProject = await utils.isPathExists(config.paths.workspaceGameProject) 262 | const onceSetup = momento.getOnceSetup() 263 | 264 | if (onceSetup) { 265 | log(`No need to suggest ${config.extension.displayName} because it's already once setup`) 266 | return false 267 | } else if (isDefoldProject) { 268 | log(`No setup has ever been done in this workspace yet, so let's suggest setup ${config.extension.displayName}`) 269 | suggestSetup(`Defold project found. Do you want to setup ${config.extension.displayName}?`, false) 270 | return true 271 | } 272 | 273 | return false 274 | } 275 | 276 | export async function offerSelectDefoldPath(): Promise { 277 | const settingsPath = config.defold?.editorPath 278 | const isSettingsPathExists = settingsPath != undefined && await utils.isPathExists(settingsPath) 279 | 280 | let suggestionItem: vscode.QuickPickItem | undefined 281 | let settingsItem: vscode.QuickPickItem | undefined 282 | 283 | const manualItem: vscode.QuickPickItem = { 284 | label: `$(file-directory) ${utils.isMac ? 'Select Defold Application' : 'Select Defold Folder'}`, 285 | detail: 'Opens the dialog to select a path manually', 286 | alwaysShow: true, 287 | } 288 | 289 | let pathItems = [manualItem] 290 | 291 | function detailForExistence(exists: boolean): string { 292 | return exists ? 'This path is valid and exists' : 'Warning: Looks like this path doesn\'t exists' 293 | } 294 | 295 | const suggestionPath = config.defoldPathSuggestion 296 | const isSuggestionPathExists = await utils.isPathExists(suggestionPath) 297 | const normalizedSuggestionPath = vscode.Uri.file(suggestionPath).fsPath 298 | 299 | if (isSuggestionPathExists && normalizedSuggestionPath != settingsPath) { 300 | suggestionItem = { 301 | label: `$(file-directory) ${suggestionPath}`, 302 | detail: detailForExistence(true), 303 | description: 'Suggestion', 304 | alwaysShow: true 305 | } 306 | 307 | pathItems.unshift(suggestionItem) 308 | } 309 | 310 | if (settingsPath) { 311 | settingsItem = { 312 | label: `$(check) ${settingsPath}`, 313 | detail: detailForExistence(isSettingsPathExists), 314 | description: 'Settings', 315 | alwaysShow: true 316 | } 317 | 318 | pathItems.unshift(settingsItem) 319 | } 320 | 321 | const selectedItem = await vscode.window.showQuickPick(pathItems, { 322 | title: `(1/4) ${config.extension.displayName} Setup`, 323 | placeHolder: 'Select a path to the Defold Editor', 324 | ignoreFocusOut: true, 325 | }) 326 | 327 | if (!selectedItem) { 328 | return 329 | } 330 | 331 | switch (selectedItem) { 332 | case settingsItem: 333 | return await config.updateDefoldPath(settingsPath ?? '') 334 | 335 | case suggestionItem: 336 | return await config.updateDefoldPath(suggestionPath) 337 | 338 | default: 339 | const defaultUri = vscode.Uri.file(isSettingsPathExists ? settingsPath : suggestionPath) 340 | 341 | const dialogResult = await vscode.window.showOpenDialog({ 342 | canSelectFiles: utils.isMac, 343 | canSelectFolders: !utils.isMac, 344 | canSelectMany: false, 345 | title: manualItem.label, 346 | defaultUri: defaultUri 347 | }) 348 | 349 | const userPath = (dialogResult || []).at(0)?.fsPath 350 | 351 | if (userPath) { 352 | return await config.updateDefoldPath(userPath) 353 | } else { 354 | return await offerSelectDefoldPath() 355 | } 356 | } 357 | } 358 | 359 | export async function offerInstallExtensions(): Promise { 360 | let extensionsItems: vscode.QuickPickItem[] = [] 361 | 362 | for (const extensionId in extensions.recommended) { 363 | const extension = extensions.recommended[extensionId as keyof typeof extensions.recommended] 364 | 365 | if (vscode.extensions.getExtension(extensionId)) { 366 | continue 367 | } 368 | 369 | extensionsItems.push({ 370 | label: `$(package) ${extension.title}`, 371 | detail: extension.detail, 372 | description: extensionId, 373 | picked: extension.picked, 374 | alwaysShow: true 375 | }) 376 | } 377 | 378 | if (extensionsItems.length == 0) { 379 | log('All the recommended extensions are already installed, so the extensions picker is skipped') 380 | return [] 381 | } 382 | 383 | momento.loadPickerSelection(extensionsItems, config.context.globalState, momento.keys.extensionInstallation) 384 | 385 | const selectedItems = await vscode.window.showQuickPick(extensionsItems, { 386 | canPickMany: true, 387 | title: `(2/4) ${config.extension.displayName} Setup`, 388 | placeHolder: 'Select extensions to install', 389 | ignoreFocusOut: true, 390 | }) 391 | 392 | if (!selectedItems) { 393 | return 394 | } 395 | 396 | await momento.savePickerSelection(extensionsItems, selectedItems, config.context.globalState, momento.keys.extensionInstallation) 397 | 398 | let extensionIds: string[] = [] 399 | 400 | await vscode.window.withProgress({ 401 | location: vscode.ProgressLocation.Notification, 402 | title: 'Installing extension' 403 | }, async progress => { 404 | for (const extensionsItem of selectedItems) { 405 | const extensionId = extensionsItem.description 406 | 407 | if (!extensionId) { 408 | continue 409 | } 410 | 411 | log(`Installing the '${extensionId}' extension.`) 412 | progress.report({ message: extensionId}) 413 | 414 | try { 415 | await vscode.commands.executeCommand('workbench.extensions.installExtension', extensionId) 416 | } catch(error) { 417 | vscode.window.showWarningMessage(`Failed to install the '${extensionId}' extension. See Output for details.`) 418 | log(`Failed to install the '${extensionId}' extension.`) 419 | log(`${error}`) 420 | continue 421 | } 422 | 423 | extensionIds.push(extensionId) 424 | } 425 | }) 426 | 427 | return extensionIds 428 | } 429 | 430 | export async function offerSyncAnnotations(defoldVersion: string, title?: string): Promise { 431 | let defoldItem: vscode.QuickPickItem = { 432 | label: '$(sync) Sync Defold API Annotations', 433 | detail: `Fetches Defold annotations and unpacks to the ${config.extension.displayName} storage`, 434 | alwaysShow: true, 435 | picked: true 436 | } 437 | 438 | let dependenciesItem: vscode.QuickPickItem = { 439 | label: '$(sync) Sync Dependencies API Annotations', 440 | detail: `Unpacks libraries and copies lua modules to the ${config.extension.displayName} storage`, 441 | alwaysShow: true, 442 | picked: true 443 | } 444 | 445 | const syncItems = [ 446 | defoldItem, 447 | dependenciesItem 448 | ] 449 | 450 | momento.loadPickerSelection(syncItems, config.context.globalState, momento.keys.settingsApplying) 451 | 452 | const selectedItems = await vscode.window.showQuickPick(syncItems, { 453 | canPickMany: true, 454 | title: title ?? `(4/4) ${config.extension.displayName} Setup`, 455 | placeHolder: 'Select API annotations for autocomplete features', 456 | ignoreFocusOut: true, 457 | }) 458 | 459 | if (!selectedItems) { 460 | return false 461 | } 462 | 463 | await momento.savePickerSelection(syncItems, selectedItems, config.context.globalState, momento.keys.settingsApplying) 464 | 465 | await vscode.window.withProgress({ 466 | location: vscode.ProgressLocation.Notification, 467 | title: 'Syncing annotations' 468 | }, async progress => { 469 | if (selectedItems.includes(defoldItem)) { 470 | progress.report({ message: 'Defold API...' }) 471 | await annotations.syncDefoldAnnotations(defoldVersion) 472 | } 473 | 474 | if (selectedItems.includes(dependenciesItem)) { 475 | progress.report({ message: 'Dependencies...' }) 476 | await annotations.syncDependenciesAnnotations() 477 | } 478 | }) 479 | 480 | return true 481 | } 482 | 483 | export async function offerApplySettings(extensionIds: string[]): Promise { 484 | let debuggerItem: vscode.QuickPickItem = { 485 | label: '$(bug) Add Debugger Scripts', 486 | detail: `Adds 'debugger.lua' and 'debugger.script' files to this workspace`, 487 | alwaysShow: true, 488 | picked: true 489 | } 490 | 491 | let launchItem: vscode.QuickPickItem = { 492 | label: '$(rocket) Add Launch Configuration', 493 | detail: `Creates or edits the '.vscode/launch.json' file in this workspace`, 494 | alwaysShow: true, 495 | picked: true 496 | } 497 | 498 | let settingsItem: vscode.QuickPickItem = { 499 | label: '$(tools) Add Recommended Workspace Settings', 500 | detail: `Creates or edits the '.vscode/settings.json' file in this workspace`, 501 | alwaysShow: true, 502 | picked: true 503 | } 504 | 505 | let recomendationsItem: vscode.QuickPickItem = { 506 | label: `$(star) Add ${config.extension.displayName} to Workspace Recommendations`, 507 | detail: `Creates or edits the '.vscode/extensions.json' file in this workspace`, 508 | alwaysShow: true, 509 | picked: true 510 | } 511 | 512 | const settingsItems = [ 513 | settingsItem, 514 | recomendationsItem 515 | ] 516 | 517 | if (extensionIds.includes(extensions.ids.localLuaDebugger) || vscode.extensions.getExtension(extensions.ids.localLuaDebugger)) { 518 | settingsItems.unshift(debuggerItem, launchItem) 519 | } 520 | 521 | momento.loadPickerSelection(settingsItems, config.context.globalState, momento.keys.settingsApplying) 522 | 523 | const selectedItems = await vscode.window.showQuickPick(settingsItems, { 524 | canPickMany: true, 525 | title: `(3/4) ${config.extension.displayName} Setup`, 526 | placeHolder: 'Select additional settings to apply', 527 | ignoreFocusOut: true, 528 | }) 529 | 530 | if (!selectedItems) { 531 | return false 532 | } 533 | 534 | await momento.savePickerSelection(settingsItems, selectedItems, config.context.globalState, momento.keys.settingsApplying) 535 | 536 | await vscode.window.withProgress({ 537 | location: vscode.ProgressLocation.Notification, 538 | title: 'Applying' 539 | }, async progress => { 540 | if (selectedItems.includes(debuggerItem)) { 541 | progress.report({ message: 'Debugger files...' }) 542 | await copyDebuggerResources() 543 | } 544 | 545 | if (selectedItems.includes(launchItem)) { 546 | progress.report({ message: 'Launch configuration...' }) 547 | await applyDebugConfigurations(extensionIds) 548 | } 549 | 550 | if (selectedItems.includes(settingsItem)) { 551 | progress.report({ message: 'Recommended settings...' }) 552 | await applyExtensionsSettings(extensionIds) 553 | } 554 | 555 | if (selectedItems.includes(recomendationsItem)) { 556 | progress.report({ message: 'Workspace recommendations...' }) 557 | await applyWorkspaceRecommendations() 558 | } 559 | }) 560 | 561 | return true 562 | } 563 | 564 | export async function offerSelectBundleTargets(): Promise { 565 | let targetItems: vscode.QuickPickItem[] = [] 566 | let targetsAdapter: { [label: string]: string } = {} 567 | 568 | for (const target in config.bundleTargets) { 569 | const targetInfo = config.bundleTargets[target as keyof typeof config.bundleTargets] 570 | 571 | const targetItem: vscode.QuickPickItem = { 572 | label: targetInfo.label, 573 | detail: `architectures: ${targetInfo.architectures}`, 574 | description: `platform: ${targetInfo.platform}`, 575 | alwaysShow: true 576 | } 577 | 578 | targetItems.push(targetItem) 579 | targetsAdapter[targetInfo.label] = target 580 | } 581 | 582 | momento.loadPickerSelection(targetItems, config.context.workspaceState, momento.keys.bundleTarget) 583 | 584 | const selectedItems = await vscode.window.showQuickPick(targetItems, { 585 | canPickMany: true, 586 | title: 'Bundle Target', 587 | placeHolder: 'Select target OS for bundling', 588 | ignoreFocusOut: false, 589 | }) 590 | 591 | if (!selectedItems || selectedItems.length == 0) { 592 | return [] 593 | } 594 | 595 | await momento.savePickerSelection(targetItems, selectedItems, config.context.workspaceState, momento.keys.bundleTarget) 596 | 597 | const targets = selectedItems.map(targetItem => { 598 | return targetsAdapter[targetItem.label] 599 | }) 600 | 601 | return targets 602 | } 603 | 604 | interface SelectedBundleOptions { 605 | isRelease: boolean, 606 | textureCompression: boolean, 607 | buildReport: boolean, 608 | debugSymbols: boolean, 609 | liveUpdate: boolean 610 | } 611 | 612 | export async function offerSelectBundleOptions(): Promise { 613 | let releaseItem: vscode.QuickPickItem = { 614 | label: '$(briefcase) Release', 615 | detail: 'Bundle Release variant (otherwise bundle Debug variant)', 616 | alwaysShow: true 617 | } 618 | 619 | let textureCompressionItem: vscode.QuickPickItem = { 620 | label: '$(file-zip) Texture Compression', 621 | detail: 'Enable texture compression as specified in texture profiles', 622 | alwaysShow: true 623 | } 624 | 625 | let debugSymbolsItem: vscode.QuickPickItem = { 626 | label: '$(key) Generate Debug Symbols', 627 | detail: 'Generate the symbol file (if applicable)', 628 | alwaysShow: true 629 | } 630 | 631 | let buildReportItem: vscode.QuickPickItem = { 632 | label: '$(checklist) Generate Build Report', 633 | detail: 'Generate the HTML build report file', 634 | alwaysShow: true 635 | } 636 | 637 | let liveUpdateItem: vscode.QuickPickItem = { 638 | label: '$(link) Publish Live Update Content', 639 | detail: 'Exclude the live update content in a separate archive', 640 | alwaysShow: true 641 | } 642 | 643 | const optionsItems = [ 644 | releaseItem, 645 | textureCompressionItem, 646 | debugSymbolsItem, 647 | buildReportItem, 648 | liveUpdateItem 649 | ] 650 | 651 | momento.loadPickerSelection(optionsItems, config.context.workspaceState, momento.keys.bundleOption) 652 | 653 | const selectedItems = await vscode.window.showQuickPick(optionsItems, { 654 | canPickMany: true, 655 | title: 'Bundle Options', 656 | placeHolder: 'Select bundle options', 657 | ignoreFocusOut: false, 658 | }) 659 | 660 | if (!selectedItems) { 661 | return 662 | } 663 | 664 | await momento.savePickerSelection(optionsItems, selectedItems, config.context.workspaceState, momento.keys.bundleOption) 665 | 666 | const options = { 667 | isRelease: selectedItems.includes(releaseItem), 668 | textureCompression: selectedItems.includes(textureCompressionItem), 669 | buildReport: selectedItems.includes(buildReportItem), 670 | debugSymbols: selectedItems.includes(debugSymbolsItem), 671 | liveUpdate: selectedItems.includes(liveUpdateItem) 672 | } 673 | 674 | return options 675 | } --------------------------------------------------------------------------------