├── .github ├── actions │ └── post-release-checklist │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── lib │ │ └── index.js.LICENSE.txt │ │ ├── action.yaml │ │ ├── webpack.config.js │ │ ├── package.json │ │ ├── src │ │ └── index.ts │ │ └── tsconfig.json ├── workflows │ ├── release_checklist.yaml │ ├── lint.yaml │ ├── codeql.yml │ └── node_js.yaml ├── linters │ └── .eslintrc.json └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitattributes ├── images ├── convertDemo.gif ├── newCommandDemo.gif ├── simulateDemo.gif ├── wpilib-with-kotlin_K-128x128.png ├── wpilib-with-kotlin_K-256x256.png └── sources │ ├── Kotlin Full Color Logo Mark RGB.svg │ ├── wpilib-edittable.svg │ └── kff-logo.svg ├── src ├── template │ ├── templates │ │ ├── emptyClass.kfftemplate │ │ ├── subsystem.kfftemplate │ │ ├── commandSkeletonRobotContainer.kfftemplate │ │ ├── instantCommand.kfftemplate │ │ ├── trapezoidProfileSubsystem.kfftemplate │ │ ├── PIDSubsystem.kfftemplate │ │ ├── parallelRaceGroup.kfftemplate │ │ ├── parallelCommandGroup.kfftemplate │ │ ├── sequentialCommandGroup.kfftemplate │ │ ├── commandAutos.kfftemplate │ │ ├── commandConstants.kfftemplate │ │ ├── parallelDeadlineGroup.kfftemplate │ │ ├── command.kfftemplate │ │ ├── profiledPIDSubsystem.kfftemplate │ │ ├── main.kfftemplate │ │ ├── trapezoidProfileCommand.kfftemplate │ │ ├── commandExampleCommand.kfftemplate │ │ ├── romiCommandExampleCommand.kfftemplate │ │ ├── PIDCommand.kfftemplate │ │ ├── timedSkeletonRobot.kfftemplate │ │ ├── exampleSubsystem.kfftemplate │ │ ├── commandSkeletonRobot.kfftemplate │ │ ├── romiCommandConstants.kfftemplate │ │ ├── profiledPIDCommand.kfftemplate │ │ ├── romiCommandRobotContainer.kfftemplate │ │ ├── romiTimedDrivetrain.kfftemplate │ │ ├── romiBuildGradle.kfftemplate │ │ ├── romiCommandDrivetrainSubsystem.kfftemplate │ │ ├── robotContainer.kfftemplate │ │ ├── robotBaseRobot.kfftemplate │ │ ├── commandRobot.kfftemplate │ │ ├── romiTimedRobot.kfftemplate │ │ ├── timedRobot.kfftemplate │ │ └── buildGradle.kfftemplate │ ├── gen_templates.ts.py │ ├── models.ts │ └── providers.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ ├── index.ts │ │ ├── commands │ │ │ ├── commands.test.ts │ │ │ └── util.test.ts │ │ └── template │ │ │ └── providers.test.ts │ └── runTest.ts ├── commands │ ├── models.ts │ ├── util.ts │ ├── commands.ts │ └── conversion.ts ├── constants.ts ├── tasks │ └── cmdExecution.ts ├── util │ ├── recommendations.ts │ ├── changelog.ts │ ├── util.ts │ └── gradleRioUpdate.ts └── extension.ts ├── .gitignore ├── RELEASE_NOTES.md ├── .vscode ├── extensions.json ├── tasks.json ├── launch.json ├── snippets.code-snippets └── settings.json ├── .eslintrc.json ├── .vscodeignore ├── tsconfig.json ├── LICENSE ├── README.md ├── webpack.config.js ├── CONTRIBUTING.md ├── package.json └── CHANGELOG.md /.github/actions/post-release-checklist/.gitignore: -------------------------------------------------------------------------------- 1 | !dist/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /images/convertDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/HEAD/images/convertDemo.gif -------------------------------------------------------------------------------- /images/newCommandDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/HEAD/images/newCommandDemo.gif -------------------------------------------------------------------------------- /images/simulateDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/HEAD/images/simulateDemo.gif -------------------------------------------------------------------------------- /images/wpilib-with-kotlin_K-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/HEAD/images/wpilib-with-kotlin_K-128x128.png -------------------------------------------------------------------------------- /images/wpilib-with-kotlin_K-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/HEAD/images/wpilib-with-kotlin_K-256x256.png -------------------------------------------------------------------------------- /src/template/templates/emptyClass.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | /** 4 | * Add your docs here. 5 | */ 6 | class #{NAME} { 7 | } 8 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/LICENSE: -------------------------------------------------------------------------------- 1 | This sub-project is licensed under the same terms as Kotlin for FRC. 2 | Please refer to it's license for more information. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | 7 | testing-workspace/template/.vscode 8 | testing-workspace/workspace/.vscode 9 | 10 | changelog.md.html 11 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Kotlin for FRC Changelog 2 | 3 | ## [next](https://github.com/BrenekH/kotlin-for-frc/releases/next) 4 | 5 | **Enhancements:** 6 | 7 | - Update GradleRIO version to 2024.3.2 8 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/lib/index.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! formdata-polyfill. MIT License. Jimmy Wärting */ 2 | 3 | /*! ws. MIT License. Einar Otto Stangvik */ 4 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | 3 | // Defines a Mocha test suite to group tests of similar kind together 4 | suite("Extension Tests", function () { 5 | // Defines a Mocha unit test 6 | test("Sample test", function () { 7 | assert.strictEqual(1, 1) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Post Release Checklist" 2 | description: "Posts a checkable list of everything that needs to happen before a version gets released." 3 | inputs: 4 | github-token: 5 | description: Github Token 6 | required: true 7 | 8 | runs: 9 | using: "node20" 10 | main: "lib/index.js" 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "aaron-bond.better-comments", 7 | "streetsidesoftware.code-spell-checker", 8 | "fabiospampinato.vscode-todo-plus" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The RobotType enum maps each type of robot project to a unique string. 3 | */ 4 | export enum RobotType { 5 | command = "command", 6 | commandSkeleton = "command_skeleton", 7 | romiCommand = "romi_command", 8 | romiTimed = "romi_timed", 9 | timed = "timed", 10 | timedSkeleton = "timed_skeleton", 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/semi": "off", 11 | "curly": "warn", 12 | "eqeqeq": "warn", 13 | "no-throw-literal": "warn", 14 | "semi": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/template/templates/subsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.SubsystemBase 4 | 5 | class #{NAME} : SubsystemBase() { 6 | override fun periodic() { 7 | // This method will be called once per scheduler run 8 | } 9 | 10 | override fun simulationPeriodic() { 11 | // This method will be called once per scheduler run during simulation 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/template/templates/commandSkeletonRobotContainer.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import edu.wpi.first.wpilibj2.command.Commands 5 | 6 | class RobotContainer { 7 | init { 8 | configureBindings() 9 | } 10 | 11 | private fun configureBindings() {} 12 | 13 | val autonomousCommand: Command 14 | get() = Commands.print("No autonomous command configured") 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release_checklist.yaml: -------------------------------------------------------------------------------- 1 | name: Release Checklist 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | comment-on-pr: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Post Release Checklist 16 | uses: ./.github/actions/post-release-checklist 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/template/templates/instantCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.InstantCommand 4 | 5 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 6 | // information, see: 7 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 8 | class #{NAME} : InstantCommand() { 9 | // Called when the command is initially scheduled. 10 | override fun initialize() { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/linters/.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 | } 20 | -------------------------------------------------------------------------------- /src/template/templates/trapezoidProfileSubsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.trajectory.TrapezoidProfile 4 | import edu.wpi.first.wpilibj2.command.TrapezoidProfileSubsystem 5 | 6 | class #{NAME} : TrapezoidProfileSubsystem( 7 | // The constraints for the generated profiles 8 | TrapezoidProfile.Constraints(0.0, 0.0), 9 | // Initial position 10 | 0.0 11 | ) { 12 | 13 | override fun useState(state: TrapezoidProfile.State) { 14 | // Use the computed profile state here. 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/template/templates/PIDSubsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.controller.PIDController 4 | import edu.wpi.first.wpilibj2.command.PIDSubsystem 5 | 6 | class #{NAME} : PIDSubsystem( 7 | // The PIDController used by the subsystem 8 | PIDController(0.0, 0.0, 0.0) 9 | ) { 10 | 11 | public override fun useOutput(output: Double, setpoint: Double) { 12 | // Use the output here 13 | } 14 | 15 | public override fun getMeasurement(): Double { 16 | // Return the process variable measurement here 17 | return 0.0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/template/templates/parallelRaceGroup.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.ParallelRaceGroup 4 | 5 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 6 | // information, see: 7 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 8 | class #{NAME} : ParallelRaceGroup() { 9 | /** 10 | * Creates a new #{NAME}. 11 | */ 12 | init { 13 | // Add your commands in the addCommands() call, e.g. 14 | // addCommands(FooCommand(), BarCommand()) 15 | addCommands() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/template/templates/parallelCommandGroup.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.ParallelCommandGroup 4 | 5 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 6 | // information, see: 7 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 8 | class #{NAME} : ParallelCommandGroup() { 9 | /** 10 | * Creates a new #{NAME}. 11 | */ 12 | init { 13 | // Add your commands in the addCommands() call, e.g. 14 | // addCommands(FooCommand(), BarCommand()) 15 | addCommands() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | src/** 5 | testing-workspace/** 6 | .github/** 7 | node_modules/** 8 | .pytest_cache/** 9 | 10 | .gitignore 11 | .gitattributes 12 | tsconfig.json 13 | vsc-extension-quickstart.md 14 | tslint.json 15 | .travis.yml 16 | package-lock.json 17 | webpack.config.js 18 | CONTRIBUTING.md 19 | 20 | TODO 21 | README.provided_by_vscode 22 | reset-testing-workspace.bat 23 | reset-testing-workspace.sh 24 | generate_changelog_html.bat 25 | RELEASE_NOTES.md 26 | 27 | **/*.vsix 28 | **/*.gif 29 | **/*.svg 30 | 31 | **/tsconfig.json 32 | **/.eslintrc.json 33 | **/*.map 34 | **/*.ts 35 | out/test/** 36 | -------------------------------------------------------------------------------- /src/template/templates/sequentialCommandGroup.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.SequentialCommandGroup 4 | 5 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 6 | // information, see: 7 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 8 | class #{NAME} : SequentialCommandGroup() { 9 | /** 10 | * Creates a new #{NAME}. 11 | */ 12 | init { 13 | // Add your commands in the addCommands() call, e.g. 14 | // addCommands(FooCommand(), BarCommand()) 15 | addCommands() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SIMULATE_CODE_TASK_NAME = "Simulate FRC Code" 2 | export const TARGET_GRADLE_RIO_YEAR = "2024" 3 | export const TARGET_GRADLE_RIO_VER = "2024.3.2" 4 | 5 | export const ROMI_CMD_VARIANT_SEARCH_TERM = `wpi.sim.envVar("HALSIMWS_HOST"` 6 | export const ROMI_TIMED_VARIANT_SEARCH_TERM = "new RomiDrivetrain()" 7 | export const CMD_ROBOT_SEARCH_TERM = "edu.wpi.first.wpilibj2.command.Command" 8 | export const CMD_ROBOT_NON_SKELE_SEARCH_TERM = "The VM is configured" 9 | export const TIMED_ROBOT_USED_SEARCH_TERM = "edu.wpi.first.wpilibj.TimedRobot" 10 | export const TIMED_ROBOT_SEARCH_TERM = "edu.wpi.first.wpilibj.smartdashboard.SendableChooser" 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "out", 6 | "lib": ["es6", "DOM"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */, 10 | /* Additional Checks */ 11 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 12 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 13 | "noUnusedParameters": true /* Report errors on unused parameters. */ 14 | }, 15 | "exclude": ["node_modules", ".vscode-test"] 16 | } 17 | -------------------------------------------------------------------------------- /src/template/templates/commandAutos.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot.commands 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import edu.wpi.first.wpilibj2.command.Commands 5 | import frc.robot.subsystems.ExampleSubsystem 6 | 7 | class Autos private constructor() { 8 | init { 9 | throw UnsupportedOperationException("This is a utility class!") 10 | } 11 | 12 | companion object { 13 | /** Example static factory for an autonomous command. */ 14 | fun exampleAuto(subsystem: ExampleSubsystem): Command { 15 | return Commands.sequence(subsystem.exampleMethodCommand(), ExampleCommand(subsystem)) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/template/templates/commandConstants.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | /** 4 | * The Constants class provides a convenient place for teams to hold robot-wide numerical or boolean 5 | * constants. This class should not be used for any other purpose. All constants should be declared 6 | * globally (i.e. inside the companion object). Do not put anything functional in this class. 7 | * 8 | * 9 | * It is advised to statically import this class (or one of its inner classes) wherever the 10 | * constants are needed, to reduce verbosity. 11 | */ 12 | class Constants { 13 | object OperatorConstants { 14 | const val kDriverControllerPort = 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: BrenekH 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when ... 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/template/templates/parallelDeadlineGroup.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.InstantCommand 4 | import edu.wpi.first.wpilibj2.command.ParallelDeadlineGroup 5 | 6 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 7 | // information, see: 8 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 9 | class #{NAME} : ParallelDeadlineGroup(InstantCommand()) { 10 | // Add the deadline to the Parent constructor. example: ParallelDeadlineGroup(MyDeadlineCommand()) 11 | 12 | init { 13 | // Add other commands using addCommands 14 | // addCommands(FooCommand(), BarCommand()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Code Base 2 | 3 | on: 4 | push: 5 | branches: [development] 6 | pull_request: 7 | branches: ["**/*"] 8 | 9 | jobs: 10 | lint-md-ts: 11 | name: Lint Markdown and Typescript 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Lint 21 | uses: super-linter/super-linter/slim@v7 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | DEFAULT_BRANCH: master 25 | VALIDATE_MARKDOWN: true 26 | VALIDATE_TYPESCRIPT_ES: true 27 | TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json 28 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/template/templates/command.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.wpilibj2.command.CommandBase 4 | 5 | class #{NAME} (): CommandBase() { 6 | /** 7 | * Creates a new #{NAME}. 8 | */ 9 | init { 10 | // Use addRequirements() here to declare subsystem dependencies. 11 | } 12 | 13 | // Called when the command is initially scheduled. 14 | override fun initialize() { } 15 | 16 | // Called every time the scheduler runs while the command is scheduled. 17 | override fun execute() { } 18 | 19 | // Called once the command ends or is interrupted. 20 | override fun end(interrupted: Boolean) { } 21 | 22 | // Returns true when the command should end. 23 | override fun isFinished(): Boolean { 24 | return false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/template/templates/profiledPIDSubsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.controller.ProfiledPIDController 4 | import edu.wpi.first.math.trajectory.TrapezoidProfile 5 | import edu.wpi.first.wpilibj2.command.ProfiledPIDSubsystem 6 | 7 | class #{NAME} : ProfiledPIDSubsystem( 8 | // The ProfiledPIDController used by the subsystem 9 | ProfiledPIDController(0.0, 0.0, 0.0, 10 | 11 | // The motion profile constraints 12 | TrapezoidProfile.Constraints(0.0, 0.0)) 13 | ) { 14 | 15 | public override fun useOutput(output: Double, setpoint: TrapezoidProfile.State) { 16 | // Use the output (and optionally the setpoint) here 17 | } 18 | 19 | public override fun getMeasurement(): Double { 20 | // Return the process variable measurement here 21 | return 0.0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise(async (c, e) => { 15 | const files = await glob("**/**.test.js", { cwd: testsRoot }); 16 | 17 | // Add files to the test suite 18 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run((failures) => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | e(err); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/template/templates/main.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.hal.FRCNetComm 4 | import edu.wpi.first.hal.HAL 5 | import edu.wpi.first.wpilibj.RobotBase 6 | 7 | /** 8 | * Do NOT add any static variables to this class, or any initialization at all. 9 | * Unless you know what you are doing, do not modify this file except to 10 | * change the parameter class to the startRobot call. 11 | */ 12 | object Main { 13 | /** 14 | * Main initialization function. Do not perform any initialization here. 15 | * 16 | * 17 | * If you change your main robot class, change the parameter type. 18 | */ 19 | @JvmStatic 20 | fun main(args: Array) { 21 | RobotBase.startRobot { 22 | HAL.report(FRCNetComm.tResourceType.kResourceType_Language, FRCNetComm.tInstances.kLanguage_Kotlin) 23 | Robot() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/template/templates/trapezoidProfileCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.trajectory.TrapezoidProfile 4 | import edu.wpi.first.wpilibj2.command.TrapezoidProfileCommand 5 | import java.util.function.Consumer 6 | 7 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 8 | // information, see: 9 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 10 | class #{NAME} : TrapezoidProfileCommand( 11 | // The motion profile to be executed 12 | TrapezoidProfile( 13 | // The motion profile constraints 14 | TrapezoidProfile.Constraints(0.0, 0.0), 15 | // Goal state 16 | TrapezoidProfile.State(), 17 | // Initial state 18 | TrapezoidProfile.State()), 19 | Consumer { state: TrapezoidProfile.State? -> { 20 | 21 | }} 22 | ) 23 | -------------------------------------------------------------------------------- /src/template/templates/commandExampleCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot.commands 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import frc.robot.subsystems.ExampleSubsystem 5 | 6 | /** An example command that uses an example subsystem. */ 7 | class ExampleCommand(subsystem: ExampleSubsystem) : Command() { 8 | init { 9 | // Use addRequirements() here to declare subsystem dependencies. 10 | addRequirements(subsystem) 11 | } 12 | 13 | /** Called when the command is initially scheduled. */ 14 | override fun initialize() {} 15 | 16 | /** Called every time the scheduler runs while the command is scheduled. */ 17 | override fun execute() {} 18 | 19 | /** Called once the command ends or is interrupted. */ 20 | override fun end(interrupted: Boolean) {} 21 | 22 | /** Returns true when the command should end. */ 23 | override fun isFinished(): Boolean { 24 | return false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const commonConfig = { 4 | output: { 5 | path: path.resolve(__dirname, "lib"), 6 | filename: "[name].js", 7 | assetModuleFilename: '[name][ext]' 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.ts$/, 13 | loader: "ts-loader" 14 | }, 15 | { 16 | test: /\.css$/, 17 | type: "asset/resource" 18 | } 19 | ], 20 | parser: { 21 | javascript: { 22 | commonjsMagicComments: true, 23 | }, 24 | }, 25 | }, 26 | resolve: { 27 | extensions: [".js", ".ts", ".tsx", ".jsx", ".json", ".css"], 28 | fallback: { 29 | "fs": false, 30 | "path": false 31 | } 32 | }, 33 | devtool: "source-map", 34 | experiments: { 35 | topLevelAwait: true, 36 | } 37 | } 38 | 39 | module.exports = [ 40 | Object.assign( 41 | { 42 | target: "electron-main", 43 | entry: { 44 | index: "./src/index.ts" 45 | } 46 | }, 47 | commonConfig 48 | ) 49 | ] -------------------------------------------------------------------------------- /src/tasks/cmdExecution.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import * as vscode from "vscode"; 3 | 4 | /** 5 | * executeCommand takes command information and runs the command in a VSCode Task. 6 | * 7 | * @param cmd Command to run 8 | * @param name Name of the command 9 | * @param workspaceFolder Workspace folder to run the command in 10 | */ 11 | export function executeCommand(cmd: string, name: string, workspaceFolder: vscode.WorkspaceFolder) { 12 | const execution = new vscode.ShellExecution(cmd, { cwd: workspaceFolder.uri.fsPath }); 13 | 14 | if (process.platform === "win32") { 15 | if (cmd.startsWith("./")) { 16 | cmd = cmd.substring(2); 17 | } 18 | execution.commandLine = cmd; 19 | if (execution.options !== undefined) { 20 | execution.options.executable = "cmd.exe"; 21 | execution.options.shellArgs = ["/d", "/c"]; 22 | } 23 | } 24 | 25 | const task = new vscode.Task({ type: "kffshell" }, workspaceFolder, name, "Kotlin-FRC", execution); 26 | vscode.tasks.executeTask(task); 27 | } 28 | -------------------------------------------------------------------------------- /src/template/templates/romiCommandExampleCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot.commands 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import frc.robot.subsystems.RomiDrivetrain 5 | 6 | /** 7 | * An example command that uses an example subsystem. 8 | * 9 | * @property subsystem 10 | */ 11 | class ExampleCommand(private val subsystem: RomiDrivetrain) : Command() { 12 | init { 13 | // Use addRequirements() here to declare subsystem dependencies. 14 | addRequirements(subsystem) 15 | } 16 | 17 | // Called when the command is initially scheduled. 18 | override fun initialize() {} 19 | 20 | // Called every time the scheduler runs while the command is scheduled. 21 | override fun execute() {} 22 | 23 | // Called once the command ends or is interrupted. 24 | override fun end(interrupted: Boolean) {} 25 | 26 | // Returns true when the command should end. 27 | override fun isFinished(): Boolean { 28 | return false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /images/sources/Kotlin Full Color Logo Mark RGB.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-release-checklist", 3 | "version": "0.0.2", 4 | "description": "Post a release checklist on a PR which releases a new version of KfF.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"hi\"", 8 | "build": "webpack --config webpack.config.js --mode production" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/BrenekH/kotlin-for-frc.git" 13 | }, 14 | "author": "Brenek Harrison", 15 | "license": "SEE LICENSE IN LICENSE", 16 | "bugs": { 17 | "url": "https://github.com/BrenekH/kotlin-for-frc/issues" 18 | }, 19 | "homepage": "https://github.com/BrenekH/kotlin-for-frc#readme", 20 | "private": true, 21 | "devDependencies": { 22 | "ts-loader": "^9.5.2", 23 | "typescript": "^5.7.3", 24 | "webpack": "^5.98.0", 25 | "webpack-cli": "^6.0.1" 26 | }, 27 | "dependencies": { 28 | "@actions/core": "^1.11.1", 29 | "@actions/github": "^6.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master", "development" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "20 22 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /src/template/templates/PIDCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.controller.PIDController 4 | import edu.wpi.first.wpilibj2.command.PIDCommand 5 | import java.util.function.DoubleConsumer 6 | import java.util.function.DoubleSupplier 7 | 8 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 9 | // information, see: 10 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 11 | class #{NAME} : PIDCommand( 12 | // The controller that the command will use 13 | PIDController(0.0, 0.0, 0.0), 14 | // This should return the measurement 15 | DoubleSupplier { 0.0 }, 16 | // This should return the setpoint (can also be a constant) 17 | DoubleSupplier { 0.0 }, 18 | // This uses the output 19 | DoubleConsumer { output: Double -> { 20 | 21 | }} 22 | ) { 23 | 24 | // Returns true when the command should end. 25 | override fun isFinished(): Boolean { 26 | return false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/template/templates/timedSkeletonRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj.TimedRobot 4 | 5 | /** 6 | * The VM is configured to automatically run this class, and to call the 7 | * functions corresponding to each mode, as described in the TimedRobot 8 | * documentation. If you change the name of this class or the package after 9 | * creating this project, you must also update the build.gradle file in the 10 | * project. 11 | */ 12 | class Robot : TimedRobot() { 13 | /** 14 | * This function is run when the robot is first started up and should be used 15 | * for any initialization code. 16 | */ 17 | override fun robotInit() {} 18 | 19 | override fun robotPeriodic() {} 20 | 21 | override fun autonomousInit() {} 22 | 23 | override fun autonomousPeriodic() {} 24 | 25 | override fun teleopInit() {} 26 | 27 | override fun teleopPeriodic() {} 28 | 29 | override fun disabledInit() {} 30 | 31 | override fun disabledPeriodic() {} 32 | 33 | override fun testInit() {} 34 | 35 | override fun testPeriodic() {} 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brenek Harrison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: test-compile" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/template/templates/exampleSubsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot.subsystems 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import edu.wpi.first.wpilibj2.command.SubsystemBase 5 | 6 | /** Creates a new ExampleSubsystem. */ 7 | class ExampleSubsystem : SubsystemBase() { 8 | /** 9 | * Example command factory method. 10 | * 11 | * @return a command 12 | */ 13 | fun exampleMethodCommand(): Command { 14 | // Inline construction of command goes here. 15 | // runOnce implicitly requires `this` subsystem. 16 | return runOnce {} 17 | } 18 | 19 | /** 20 | * An example method querying a boolean state of the subsystem (for example, a digital sensor). 21 | * 22 | * @return value of some boolean subsystem state, such as a digital sensor. 23 | */ 24 | fun exampleCondition(): Boolean { 25 | // Query some boolean state, such as a digital sensor. 26 | return false 27 | } 28 | 29 | /** This method will be called once per scheduler run */ 30 | override fun periodic() { 31 | } 32 | 33 | /** This method will be called once per scheduler run during simulation */ 34 | override fun simulationPeriodic() { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/suite/commands/commands.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as vscode from "vscode" 3 | import { determinePackage, simulateFRCKotlinCode } from "../../../commands/commands" 4 | 5 | suite("Simulate FRC Kotlin Code", function () { 6 | test("Returns a function", function () { 7 | let cmdExecutor = { 8 | execute: (_: string, __: string, ___: vscode.WorkspaceFolder) => { } 9 | } 10 | 11 | const result = simulateFRCKotlinCode(cmdExecutor) 12 | assert.strictEqual(typeof result, typeof ((..._: any[]) => { })) 13 | }) 14 | }) 15 | 16 | suite("Determine package", function () { 17 | test("Basic usage", function () { 18 | const dir = vscode.Uri.file("/home/test/project/src/main/kotlin/frc/robot") 19 | 20 | assert.strictEqual(determinePackage(dir), "frc.robot") 21 | }) 22 | 23 | test("Non-\"root\" folder", function () { 24 | const workDir = vscode.workspace.workspaceFolders 25 | if (workDir === undefined) { 26 | console.warn("No open workspaces for Determine package: Non-\"root\" folder to run") 27 | return 28 | } 29 | 30 | const dir = vscode.Uri.joinPath(workDir[0].uri, "src", "main", "kotlin", "frc", "robot", "subsystems") 31 | 32 | assert.strictEqual(determinePackage(dir), "frc.robot.subsystems") 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin for FIRST Robotics Competition 2 | 3 | Kotlin for FRC is an unofficial companion extension to the WPILib extension. It adds various support features so that FRC teams can program effectively in Kotlin using Visual Studio Code. 4 | 5 | ## Features 6 | 7 | - Easily convert new Java projects created by WPILib to Kotlin ones 8 | 9 | ![Convert Demo](https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/master/images/convertDemo.gif) 10 | 11 | - Quickly generate code files from templates just like WPILib does for C++ and Java 12 | 13 | ![New Command Demo](https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/master/images/newCommandDemo.gif) 14 | 15 | - Simulate Kotlin code in the FRC Sim GUI 16 | 17 | ![Simulate Kotlin Code Demo](https://raw.githubusercontent.com/BrenekH/kotlin-for-frc/master/images/simulateDemo.gif) 18 | 19 | ## Contributing 20 | 21 | Kotlin for FRC uses [GitHub Discussions](https://github.com/BrenekH/kotlin-for-frc/discussions) to talk and ask questions and [GitHub Issues](https://github.com/BrenekH/kotlin-for-frc/issues) to track bugs and features. 22 | 23 | Developer information, including project structure and dependencies, can be found in [CONTRIBUTING.md](https://github.com/BrenekH/kotlin-for-frc/blob/master/CONTRIBUTING.md). 24 | -------------------------------------------------------------------------------- /src/template/templates/commandSkeletonRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import edu.wpi.first.wpilibj2.command.CommandScheduler 5 | import edu.wpi.first.wpilibj.TimedRobot 6 | 7 | class Robot : TimedRobot() { 8 | private var autonomousCommand: Command? = null 9 | private var robotContainer: RobotContainer? = null 10 | 11 | override fun robotInit() { 12 | robotContainer = RobotContainer() 13 | } 14 | 15 | override fun robotPeriodic() { 16 | CommandScheduler.getInstance().run() 17 | } 18 | 19 | override fun disabledInit() {} 20 | 21 | override fun disabledPeriodic() {} 22 | 23 | override fun disabledExit() {} 24 | 25 | override fun autonomousInit() { 26 | autonomousCommand = robotContainer?.autonomousCommand 27 | 28 | autonomousCommand?.schedule() 29 | } 30 | 31 | override fun autonomousPeriodic() {} 32 | 33 | override fun autonomousExit() {} 34 | 35 | override fun teleopInit() { 36 | autonomousCommand?.cancel() 37 | } 38 | 39 | override fun teleopPeriodic() {} 40 | 41 | override fun teleopExit() {} 42 | 43 | override fun testInit() { 44 | CommandScheduler.getInstance().cancelAll() 45 | } 46 | 47 | override fun testPeriodic() {} 48 | 49 | override fun testExit() {} 50 | } 51 | -------------------------------------------------------------------------------- /src/template/gen_templates.ts.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | """gen_templates.ts.py reads from the templates directory and creates templates.ts with the contents of each file bound to a unique variable. 3 | """ 4 | 5 | from pathlib import Path 6 | 7 | 8 | def main(): 9 | search_dir = Path(__file__).parent / "templates" 10 | 11 | found_files = {} 12 | for p in search_dir.glob("*.kfftemplate"): 13 | with p.open("r") as f: 14 | found_files[p.name.replace(".kfftemplate", "")] = f.read() 15 | 16 | templates_ts_contents = """/** 17 | * This file was generated using gen_templates.ts.py from the templates directory. 18 | * Please do not change manually. Instead, modify the template files and then re-generate this file. 19 | */ 20 | 21 | /** 22 | * TemplateStrings is an auto-generated class containing the raw values of the master 23 | * .kfftemplate files for use by the IntegratedTemplateProvider. 24 | */ 25 | export class TemplateStrings { 26 | """ 27 | 28 | for key in found_files: 29 | value: str = found_files[key] 30 | templates_ts_contents += f" {key} = `{value.replace('`', '')}`\n" 31 | 32 | templates_ts_contents += "}\n" 33 | 34 | templates_ts = Path(__file__).parent / "templates.ts" 35 | with templates_ts.open("w") as f: 36 | f.write(templates_ts_contents) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /src/template/templates/romiCommandConstants.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | /** 4 | * The Constants class provides a convenient place for teams to hold robot-wide numerical or boolean 5 | * constants. This class should not be used for any other purpose. All constants should be 6 | * declared globally (i.e. inside the companion object). Do not put anything functional in this class. 7 | * 8 | * 9 | * It is advised to statically import this class (or one of its inner classes) wherever the 10 | * constants are needed, to reduce verbosity. 11 | */ 12 | class Constants { 13 | companion object { 14 | // Put constant values inside the companion object to make them globally accessible. 15 | // ex. val motorPort: Int = 0 16 | } 17 | 18 | class Drivetrain { 19 | companion object { 20 | // The Romi has the left and right motors set to 21 | // PWM channels 0 and 1 respectively 22 | const val LEFT_MOTOR_CHANNEL = 0 23 | const val RIGHT_MOTOR_CHANNEL = 1 24 | 25 | // The Romi has onboard encoders that are hardcoded 26 | // to use DIO pins 4/5 and 6/7 for the left and right 27 | const val LEFT_ENCODER_A = 4 28 | const val LEFT_ENCODER_B = 5 29 | const val RIGHT_ENCODER_A = 6 30 | const val RIGHT_ENCODER_B = 7 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/template/templates/profiledPIDCommand.kfftemplate: -------------------------------------------------------------------------------- 1 | package #{PACKAGE} 2 | 3 | import edu.wpi.first.math.controller.ProfiledPIDController 4 | import edu.wpi.first.math.trajectory.TrapezoidProfile 5 | import edu.wpi.first.wpilibj2.command.ProfiledPIDCommand 6 | import java.util.function.BiConsumer 7 | import java.util.function.DoubleSupplier 8 | import java.util.function.Supplier 9 | 10 | // NOTE: Consider using this command inline, rather than writing a subclass. For more 11 | // information, see: 12 | // https://docs.wpilib.org/en/stable/docs/software/commandbased/convenience-features.html 13 | class #{NAME} : ProfiledPIDCommand( 14 | // The ProfiledPIDController used by the command 15 | ProfiledPIDController( 16 | // The PID gains 17 | 0.0, 0.0, 0.0, 18 | // The motion profile constraints 19 | TrapezoidProfile.Constraints(0.0, 0.0)), 20 | // This should return the measurement 21 | DoubleSupplier { 0.0 }, 22 | // This should return the goal (can also be a constant) 23 | Supplier { TrapezoidProfile.State() }, 24 | // This uses the output 25 | BiConsumer { output: Double?, setpoint: TrapezoidProfile.State? -> { 26 | 27 | }} 28 | ) { 29 | 30 | // Returns true when the command should end. 31 | override fun isFinished(): Boolean { 32 | return false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/util/recommendations.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * ensureExtensionsRecommended will ensure that the provided extensions are recommended through the 5 | * workspaceDir.uri/.vscode/extensions.json file. 6 | * 7 | * This has only been tested on a non-multi-root workspace. Support for .code-workspace files is a not 8 | * implemented. 9 | * 10 | * @param workspaceDir The parent folder of the target .vscode/extensions.json 11 | * @param extensions The extensions to make sure exist in .vscode/extensions.json 12 | */ 13 | export async function ensureExtensionsRecommended(workspaceDir: vscode.WorkspaceFolder, extensions: string[]) { 14 | // TODO: Make sure this function works with .code-workspace files and multi-root workspaces 15 | 16 | const targetURI = vscode.Uri.joinPath(workspaceDir.uri, ".vscode", "extensions.json") 17 | 18 | let extJSON: { recommendations: string[] } 19 | try { 20 | const extJSONData = await vscode.workspace.fs.readFile(targetURI) 21 | 22 | extJSON = JSON.parse(Buffer.from(extJSONData).toString("utf8")) as { recommendations: string[] } 23 | } catch { 24 | extJSON = { recommendations: [] } 25 | } 26 | 27 | extensions.forEach(extID => { 28 | if (extJSON.recommendations.indexOf(extID) === -1) { 29 | extJSON.recommendations.push(extID) 30 | } 31 | }); 32 | 33 | const extJSONStr = JSON.stringify(extJSON, null, 2) 34 | 35 | await vscode.workspace.fs.writeFile(targetURI, Buffer.from(extJSONStr, 'utf-8')) 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/snippets.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "Create Mocha Test": { 19 | "prefix": "mochatest", 20 | "body": [ 21 | "test(\"$1\", function(){$0});" 22 | ], 23 | "description": "Create a mocha test", 24 | "scope": "typescript" 25 | }, 26 | "Create Kotlin-For-FRC Mocha Test": { 27 | "prefix": "kfftestgrabtemplate", 28 | "body": [ 29 | "test(\"$1\", function() {", 30 | "\ttestingConsts.resetTestingWorkspace();", 31 | "\tvar $2 = new $0;", 32 | "\tassert.equal(template_interpreter.getTemplateObjectFromTemplateType(template_interpreter.templateType.$3).getText(), $2.getText());", 33 | "});" 34 | ], 35 | "description": "Create a template grabbing test specific to kotlin-for-frc", 36 | "scope": "typescript" 37 | } 38 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | 4 | 'use strict'; 5 | 6 | const path = require('path'); 7 | 8 | /**@type {import('webpack').Configuration}*/ 9 | const config = { 10 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 11 | 12 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | output: { 14 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'extension.js', 17 | libraryTarget: 'commonjs2', 18 | devtoolModuleFilenameTemplate: '../[resource-path]' 19 | }, 20 | devtool: 'source-map', 21 | externals: { 22 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 23 | }, 24 | resolve: { 25 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 26 | extensions: ['.ts', '.js'], 27 | 28 | // follow-redirects (dependency of axios) uses 'debug' instead of './debug', causing a warning from webpack 29 | preferRelative: true, 30 | 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: 'ts-loader' 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | }; 46 | module.exports = config; 47 | -------------------------------------------------------------------------------- /src/template/templates/romiCommandRobotContainer.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import frc.robot.commands.ExampleCommand 5 | import frc.robot.subsystems.RomiDrivetrain 6 | 7 | /** 8 | * This class is where the bulk of the robot should be declared. Since Command-based is a 9 | * "declarative" paradigm, very little robot logic should actually be handled in the [Robot] 10 | * periodic methods (other than the scheduler calls). Instead, the structure of the robot 11 | * (including subsystems, commands, and button mappings) should be declared here. 12 | */ 13 | class RobotContainer { 14 | // The robot's subsystems and commands are defined here... 15 | private val romiDrivetrain = RomiDrivetrain() 16 | private val autoCommand = ExampleCommand(romiDrivetrain) 17 | 18 | /** 19 | * The container for the robot. Contains subsystems, OI devices, and commands. 20 | */ 21 | init { 22 | // Configure the button bindings 23 | configureButtonBindings() 24 | } 25 | 26 | /** 27 | * Use this method to define your button->command mappings. Buttons can be created by 28 | * instantiating a [edu.wpi.first.wpilibj.GenericHID] or one of its subclasses ([edu.wpi.first.wpilibj.Joystick] or [XboxController]), and then passing it to a 29 | * [edu.wpi.first.wpilibj2.command.button.JoystickButton]. 30 | */ 31 | private fun configureButtonBindings() {} 32 | 33 | /** 34 | * Use this to pass the autonomous command to the main [Robot] class. 35 | * 36 | * @return the command to run in autonomous 37 | */ 38 | val autonomousCommand: Command 39 | get() { 40 | // An ExampleCommand will run in autonomous 41 | return autoCommand 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import * as github from "@actions/github" 3 | 4 | async function run(): Promise { 5 | try { 6 | 7 | const context = github.context; 8 | const octokit = github.getOctokit(core.getInput("github-token")); 9 | 10 | if (context.eventName !== "pull_request") { 11 | throw "Not a pull request" 12 | } 13 | 14 | if (context.payload.action !== "opened") { 15 | throw "Action can only run on Pull Request open events." 16 | } 17 | 18 | const pull_request = (context.payload.pull_request as { 19 | [key: string]: any; 20 | number: number; 21 | html_url?: string; 22 | body?: string; 23 | }) 24 | 25 | // Check if merging branch is master 26 | if (pull_request["base"].ref !== "master") { 27 | console.log("Base ref is not master") 28 | return 29 | } 30 | 31 | // Check if current branch is of the form (release-(semver)) 32 | // Uses a regex from semver.org to match the semver portion of the release branch name 33 | if (!/^release-(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(pull_request["head"].ref)) { 34 | console.log("Head ref does not match the release regex") 35 | return 36 | } 37 | 38 | // Comment on the PR 39 | octokit.rest.issues.createComment({ 40 | issue_number: context.issue.number, 41 | owner: context.repo.owner, 42 | repo: context.repo.repo, 43 | body: "It looks like you've opened a release PR!\nHere's a handy checklist of things that need to be done before a release.\n\n- [ ] Update RELEASE_NOTES.md\n- [ ] Update CHANGELOG.md\n- [ ] Update Changelog Webview HTML\n- [ ] Update version" 44 | }) 45 | 46 | } catch (error: any) { 47 | core.setFailed(error) 48 | } 49 | } 50 | 51 | run() 52 | -------------------------------------------------------------------------------- /src/template/templates/romiTimedDrivetrain.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj.Encoder 4 | import edu.wpi.first.wpilibj.drive.DifferentialDrive 5 | import edu.wpi.first.wpilibj.motorcontrol.Spark 6 | 7 | class RomiDrivetrain { 8 | // The Romi has the left and right motors set to 9 | // PWM channels 0 and 1 respectively 10 | private val leftMotor = Spark(0) 11 | private val rightMotor = Spark(1) 12 | 13 | // The Romi has onboard encoders that are hardcoded 14 | // to use DIO pins 4/5 and 6/7 for the left and right 15 | private val leftEncoder = Encoder(4, 5) 16 | private val rightEncoder = Encoder(6, 7) 17 | 18 | // Set up the differential drive controller 19 | private val diffDrive = DifferentialDrive(leftMotor, rightMotor) 20 | 21 | companion object { 22 | private const val COUNTS_PER_REVOLUTION = 1440.0 23 | private const val WHEEL_DIAMETER_INCH = 2.75 24 | } 25 | 26 | /** 27 | * Creates a new RomiDrivetrain. 28 | */ 29 | init { 30 | leftEncoder.distancePerPulse = (Math.PI * WHEEL_DIAMETER_INCH) / COUNTS_PER_REVOLUTION 31 | rightEncoder.distancePerPulse = (Math.PI * WHEEL_DIAMETER_INCH) / COUNTS_PER_REVOLUTION 32 | resetEncoders() 33 | } 34 | 35 | fun arcadeDrive(xaxisSpeed: Double, zaxisRotate: Double) { 36 | diffDrive.arcadeDrive(xaxisSpeed, zaxisRotate) 37 | } 38 | 39 | fun resetEncoders() { 40 | leftEncoder.reset() 41 | rightEncoder.reset() 42 | } 43 | 44 | // The Kotlin getter pattern is used here so that the value updates everytime the property is accessed 45 | val leftEncoderCount: Int 46 | get() = leftEncoder.get() 47 | val rightEncoderCount: Int 48 | get() = rightEncoder.get() 49 | val leftDistanceInch: Double 50 | get() = Math.PI * WHEEL_DIAMETER_INCH * (leftEncoderCount / COUNTS_PER_REVOLUTION) 51 | val rightDistanceInch: Double 52 | get() = Math.PI * WHEEL_DIAMETER_INCH * (rightEncoderCount / COUNTS_PER_REVOLUTION) 53 | } 54 | -------------------------------------------------------------------------------- /src/template/templates/romiBuildGradle.kfftemplate: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "edu.wpi.first.GradleRIO" version "#{GRADLE_RIO_VERSION}" 4 | id "org.jetbrains.kotlin.jvm" version "1.6.10" 5 | } 6 | 7 | sourceCompatibility = JavaVersion.VERSION_11 8 | targetCompatibility = JavaVersion.VERSION_11 9 | 10 | def ROBOT_MAIN_CLASS = "frc.robot.Main" 11 | 12 | // Set this to true to enable desktop support. 13 | def includeDesktopSupport = true 14 | 15 | // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. 16 | // Also defines JUnit 4. 17 | dependencies { 18 | implementation wpi.java.deps.wpilib() 19 | implementation wpi.java.vendor.java() 20 | 21 | nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) 22 | nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) 23 | simulationDebug wpi.sim.enableDebug() 24 | 25 | nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) 26 | nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) 27 | simulationRelease wpi.sim.enableRelease() 28 | 29 | testImplementation 'junit:junit:4.12' 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 31 | } 32 | 33 | // Simulation configuration (e.g. environment variables). 34 | wpi.sim.addGui().defaultEnabled = true 35 | wpi.sim.addDriverstation() 36 | 37 | //Sets the websocket client remote host. 38 | wpi.sim.envVar("HALSIMWS_HOST", "10.0.0.2") 39 | wpi.sim.addWebsocketsServer().defaultEnabled = true 40 | wpi.sim.addWebsocketsClient().defaultEnabled = true 41 | 42 | // Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') 43 | // in order to make them all available at runtime. Also adding the manifest so WPILib 44 | // knows where to look for our Robot Class. 45 | jar { 46 | from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } 47 | manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) 48 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 49 | } 50 | 51 | wpi.java.configureExecutableTasks(jar) 52 | wpi.java.configureTestTasks(test) 53 | 54 | repositories { 55 | mavenCentral() 56 | } 57 | 58 | compileKotlin { 59 | kotlinOptions { 60 | jvmTarget = "11" 61 | } 62 | } 63 | 64 | compileTestKotlin { 65 | kotlinOptions { 66 | jvmTarget = "11" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/template/templates/romiCommandDrivetrainSubsystem.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot.subsystems 2 | 3 | import edu.wpi.first.wpilibj.Encoder 4 | import edu.wpi.first.wpilibj.drive.DifferentialDrive 5 | import edu.wpi.first.wpilibj.motorcontrol.Spark 6 | import edu.wpi.first.wpilibj2.command.SubsystemBase 7 | import frc.robot.Constants 8 | 9 | class RomiDrivetrain : SubsystemBase() { 10 | private val leftMotor = Spark(Constants.Drivetrain.LEFT_MOTOR_CHANNEL) 11 | private val rightMotor = Spark(Constants.Drivetrain.RIGHT_MOTOR_CHANNEL) 12 | 13 | private val leftEncoder = Encoder(Constants.Drivetrain.LEFT_ENCODER_A, Constants.Drivetrain.LEFT_ENCODER_B) 14 | private val rightEncoder = Encoder(Constants.Drivetrain.RIGHT_ENCODER_A, Constants.Drivetrain.RIGHT_ENCODER_B) 15 | 16 | // Set up the differential drive controller 17 | private val diffDrive = DifferentialDrive(leftMotor, rightMotor) 18 | 19 | companion object { 20 | private const val kCountsPerRevolution = 1440.0 21 | private const val kWheelDiameterInch = 2.75 22 | } 23 | 24 | /** 25 | * Creates a new RomiDrivetrain. 26 | */ 27 | init { 28 | leftEncoder.distancePerPulse = (Math.PI * kWheelDiameterInch) / kCountsPerRevolution 29 | rightEncoder.distancePerPulse = (Math.PI * kWheelDiameterInch) / kCountsPerRevolution 30 | resetEncoders() 31 | } 32 | 33 | fun arcadeDrive(xAxisSpeed: Double, zAxisRotate: Double) = diffDrive.arcadeDrive(xAxisSpeed, zAxisRotate) 34 | 35 | fun resetEncoders() { 36 | leftEncoder.reset() 37 | rightEncoder.reset() 38 | } 39 | 40 | // The Kotlin getter pattern is used here so that the value updates every time the property is accessed 41 | val leftEncoderCount: Int 42 | get() = leftEncoder.get() 43 | val rightEncoderCount: Int 44 | get() = rightEncoder.get() 45 | val leftDistanceInch: Double 46 | get() = Math.PI * kWheelDiameterInch * (leftEncoderCount / kCountsPerRevolution) 47 | val rightDistanceInch: Double 48 | get() = Math.PI * kWheelDiameterInch * (rightEncoderCount / kCountsPerRevolution) 49 | 50 | override fun periodic() { 51 | // This method will be called once per scheduler run 52 | } 53 | 54 | override fun simulationPeriodic() { 55 | // This method will be called once per scheduler run during simulation 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { RobotType } from "./models" 3 | import { 4 | CMD_ROBOT_SEARCH_TERM, TIMED_ROBOT_USED_SEARCH_TERM, TIMED_ROBOT_SEARCH_TERM, 5 | ROMI_CMD_VARIANT_SEARCH_TERM, ROMI_TIMED_VARIANT_SEARCH_TERM, CMD_ROBOT_NON_SKELE_SEARCH_TERM 6 | } from "../constants" 7 | 8 | /** 9 | * determineRobotType figures out the RobotType of the project by parsing through the contents of Robot.java and build.gradle. 10 | * 11 | * @param robotJava Contents of Robot.java 12 | * @param buildGradle Contents of build.gradle 13 | * @returns The determined RobotType of the project 14 | */ 15 | export function determineRobotType(robotJava: string, buildGradle: string): RobotType { 16 | let currentRobotType: RobotType = RobotType.timed 17 | 18 | if (robotJava.includes(CMD_ROBOT_SEARCH_TERM)) { 19 | if (buildGradle.includes(ROMI_CMD_VARIANT_SEARCH_TERM)) { 20 | currentRobotType = RobotType.romiCommand 21 | } 22 | else if (robotJava.includes(CMD_ROBOT_NON_SKELE_SEARCH_TERM)) { 23 | currentRobotType = RobotType.command 24 | } 25 | else { 26 | currentRobotType = RobotType.commandSkeleton 27 | } 28 | } else if (robotJava.includes(TIMED_ROBOT_USED_SEARCH_TERM)) { 29 | if (robotJava.includes(ROMI_TIMED_VARIANT_SEARCH_TERM)) { 30 | currentRobotType = RobotType.romiTimed 31 | } 32 | else if (robotJava.includes(TIMED_ROBOT_SEARCH_TERM)) { 33 | currentRobotType = RobotType.timed 34 | } 35 | else { 36 | currentRobotType = RobotType.timedSkeleton 37 | } 38 | } 39 | 40 | return currentRobotType 41 | } 42 | 43 | /** 44 | * parseTemplate replaces #{NAME}, #{PACKAGE}, and #{GRADLE_RIO_VERSION} with the provided values. 45 | * 46 | * @param template String to use as source 47 | * @param name The class name or file name to use 48 | * @param packageName Package of new file ('frc.robot') 49 | * @param gradleRioVersion Latest GradleRIO version 50 | * @returns Parsed string 51 | */ 52 | export function parseTemplate(template: string, name: string, packageName: string, gradleRioVersion: string): string { 53 | return template.replace(/#{NAME}/gi, name).replace(/#{PACKAGE}/gi, packageName).replace(/#{GRADLE_RIO_VERSION}/gi, gradleRioVersion) 54 | } 55 | 56 | /** 57 | * createFileWithContent writes to a file with the provided content. 58 | * 59 | * @param file File location 60 | * @param content Content to save to file 61 | */ 62 | export async function createFileWithContent(file: vscode.Uri, content: string): Promise { 63 | const data = Buffer.from(content, "utf8") 64 | return vscode.workspace.fs.writeFile(file, data) 65 | } 66 | -------------------------------------------------------------------------------- /src/test/suite/commands/util.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import { RobotType } from "../../../commands/models" 3 | import { determineRobotType, parseTemplate } from "../../../commands/util" 4 | 5 | suite("Determine Robot Type", function () { 6 | test("Command based", function () { 7 | let result = determineRobotType("edu.wpi.first.wpilibj2.command.Command The VM is configured", "") 8 | 9 | assert.strictEqual(result, RobotType.command) 10 | }) 11 | 12 | test("Romi command based", function () { 13 | let result = determineRobotType("edu.wpi.first.wpilibj2.command.Command", `wpi.sim.envVar("HALSIMWS_HOST", "10.0.0.2")`) 14 | 15 | assert.strictEqual(result, RobotType.romiCommand) 16 | }) 17 | 18 | test("Timed skeleton", function () { 19 | let result = determineRobotType("edu.wpi.first.wpilibj.TimedRobot", "") 20 | 21 | assert.strictEqual(result, RobotType.timedSkeleton) 22 | }) 23 | 24 | test("Timed", function () { 25 | let result = determineRobotType("edu.wpi.first.wpilibj.TimedRobot edu.wpi.first.wpilibj.smartdashboard.SendableChooser", "") 26 | 27 | assert.strictEqual(result, RobotType.timed) 28 | }) 29 | 30 | test("Romi timed", function () { 31 | let result = determineRobotType("edu.wpi.first.wpilibj.TimedRobot new RomiDrivetrain()", "") 32 | 33 | assert.strictEqual(result, RobotType.romiTimed) 34 | }) 35 | 36 | test("Command based skeleton", function () { 37 | let result = determineRobotType("edu.wpi.first.wpilibj2.command.Command", "") 38 | 39 | assert.strictEqual(result, RobotType.commandSkeleton) 40 | }) 41 | }) 42 | 43 | suite("Parse Template", function () { 44 | test("Class Name", function () { 45 | let result = parseTemplate("#{NAME}", "TestName", "frc.robot", "9999.9.9") 46 | assert.strictEqual(result, "TestName") 47 | 48 | result = parseTemplate("class #{NAME} {}", "TestName", "frc.robot", "9999.9.9") 49 | assert.strictEqual(result, "class TestName {}") 50 | }) 51 | 52 | test("Package name", function () { 53 | let result = parseTemplate("#{PACKAGE}", "TestName", "frc.robot", "9999.9.9") 54 | assert.strictEqual(result, "frc.robot") 55 | 56 | result = parseTemplate("package #{PACKAGE};", "TestName", "frc.robot", "9999.9.9") 57 | assert.strictEqual(result, "package frc.robot;") 58 | }) 59 | 60 | test("GradleRIO version", function () { 61 | let result = parseTemplate("#{GRADLE_RIO_VERSION}", "TestName", "frc.robot", "9999.9.9") 62 | assert.strictEqual(result, "9999.9.9") 63 | 64 | result = parseTemplate("edu.wpilib.GradleRio #{GRADLE_RIO_VERSION};", "TestName", "frc.robot", "9999.9.9") 65 | assert.strictEqual(result, "edu.wpilib.GradleRio 9999.9.9;") 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/template/templates/robotContainer.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj2.command.Command 4 | import edu.wpi.first.wpilibj2.command.button.CommandXboxController 5 | import edu.wpi.first.wpilibj2.command.button.Trigger 6 | import frc.robot.commands.Autos 7 | import frc.robot.commands.ExampleCommand 8 | import frc.robot.subsystems.ExampleSubsystem 9 | 10 | /** 11 | * This class is where the bulk of the robot should be declared. Since Command-based is a 12 | * "declarative" paradigm, very little robot logic should actually be handled in the [Robot] 13 | * periodic methods (other than the scheduler calls). Instead, the structure of the robot (including 14 | * subsystems, commands, and trigger mappings) should be declared here. 15 | */ 16 | class RobotContainer { 17 | // The robot's subsystems and commands are defined here... 18 | private val exampleSubsystem = ExampleSubsystem() 19 | 20 | // Replace with CommandPS4Controller or CommandJoystick if needed 21 | private val driverController = CommandXboxController(Constants.OperatorConstants.kDriverControllerPort) 22 | 23 | /** The container for the robot. Contains subsystems, OI devices, and commands. */ 24 | init { 25 | // Configure the trigger bindings 26 | configureBindings() 27 | } 28 | 29 | /** 30 | * Use this method to define your trigger->command mappings. Triggers can be created via the 31 | * [Trigger#Trigger(java.util.function.BooleanSupplier)] constructor with an arbitrary 32 | * predicate, or via the named factories in [edu.wpi.first.wpilibj2.command.button.CommandGenericHID]'s subclasses for 33 | * [CommandXboxController]/[edu.wpi.first.wpilibj2.command.button.CommandPS4Controller] controllers 34 | * or [edu.wpi.first.wpilibj2.command.button.CommandJoystick]. 35 | */ 36 | private fun configureBindings() { 37 | // Schedule `ExampleCommand` when `exampleCondition` changes to `true` 38 | Trigger { exampleSubsystem.exampleCondition() }.onTrue(ExampleCommand(exampleSubsystem)) 39 | 40 | // Schedule `exampleMethodCommand` when the Xbox controller's B button is pressed, 41 | // cancelling on release. 42 | driverController.b().whileTrue(exampleSubsystem.exampleMethodCommand()) 43 | } 44 | 45 | /** 46 | * Use this to pass the autonomous command to the main [Robot] class. 47 | * 48 | * @return the command to run in autonomous 49 | */ 50 | val autonomousCommand: Command 51 | get() { 52 | // An example command will be run in autonomous 53 | return Autos.exampleAuto(exampleSubsystem) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/util/changelog.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import * as semver from "semver" 3 | 4 | /** 5 | * displays the changelog using a webview only if the extension was updated and showChangelogOnUpdate is true. 6 | * 7 | * @param showChangelogOnUpdate Whether or not to show the changelog when the extension is updated 8 | * @param context VSCode extension context 9 | */ 10 | export function displayChangelog(showChangelogOnUpdate: boolean, context: vscode.ExtensionContext) { 11 | if (showChangelogOnUpdate && extensionWasUpdated(context)) { 12 | showChangelog() 13 | } 14 | } 15 | 16 | /** 17 | * extensionWasUpdated uses VSCode's global states to determine if the last version that was run on this 18 | * machine was older than the current running extension. 19 | * 20 | * @param context VSCode extension context, used to remember the last version run on the current machine 21 | * @returns Whether or not the extension was updated 22 | */ 23 | function extensionWasUpdated(context: vscode.ExtensionContext): boolean { 24 | const thisExtension = vscode.extensions.getExtension('brenek.kotlin-for-frc') 25 | if (thisExtension === undefined) { 26 | console.error("thisExtension was undefined, the changelog will not be displayed.") 27 | return false 28 | } 29 | 30 | const currentVersion = thisExtension.packageJSON["version"] 31 | const storedVersion = context.globalState.get("lastInitVersion", "0.0.0") 32 | 33 | context.globalState.update("lastInitVersion", currentVersion) 34 | 35 | return semver.satisfies(currentVersion, `>${storedVersion}`) 36 | } 37 | 38 | /** 39 | * showChangelog displays the latest release notes (stored in the webviewContent constant) using a VSCode webview panel. 40 | */ 41 | export function showChangelog() { 42 | const panel = vscode.window.createWebviewPanel('kotlin-for-frcChangelog', 'Kotlin For FRC Changelog', vscode.ViewColumn.One, {}) 43 | 44 | panel.webview.html = webviewContent 45 | } 46 | 47 | const webviewContent = ` 48 | 49 | 50 | 51 | 52 | Kotlin For FRC Changelog 53 | 54 | 55 |

Kotlin for FRC Changelog

56 |

2024.1.1

57 |

Enhancements:

58 |
    59 |
  • Update templates for 2024
  • 60 |
  • Add Command Based Skeleton template and remove Robot Base template
  • 61 |
  • Report projects as Kotlin instead of Java (#151)
  • 62 |
63 | 64 | 65 | ` 66 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { homedir } from "os"; 3 | import * as vscode from "vscode"; 4 | import { registerCommands } from "./commands/commands"; 5 | import { FileSystemTemplateProvider, IntegratedTemplateProvider, TemplateProviderAggregator } from "./template/providers"; 6 | import { displayChangelog } from "./util/changelog"; 7 | import updateGradleRioVersion from "./util/gradleRioUpdate"; 8 | import { addCurrentWorkspaceDirsToAggregator, alertForMissingWPILibExt, setIsKFFProject } from "./util/util"; 9 | 10 | /** 11 | * activate is the first function to be run by theVSCode Extension Host when the extension is loaded. 12 | * 13 | * @param context The extension context passed to the extension by the Extension Host. 14 | */ 15 | export async function activate(context: vscode.ExtensionContext) { 16 | // Setup 17 | setIsKFFProject() 18 | 19 | // Read updateGradleRIOVer and showChangelogOnUpdate from vscode settings, taking care to set them to default values in case of an undefined value. 20 | let temp: boolean | undefined = vscode.workspace.getConfiguration("kotlinForFRC.gradleRioVersion").get("autoUpdate") 21 | const updateGradleRIOVer: boolean = temp !== undefined ? temp : true 22 | temp = vscode.workspace.getConfiguration("kotlinForFRC.changelog").get("showOnUpdate") 23 | const showChangelogOnUpdate: boolean = temp !== undefined ? temp : true 24 | 25 | // Setup template providers 26 | const integratedTemplateProv = new IntegratedTemplateProvider() 27 | const userTemplateProv = new FileSystemTemplateProvider(vscode.Uri.joinPath(vscode.Uri.file(homedir()), ".kfftemplates")) 28 | const templateProvAgg = new TemplateProviderAggregator(integratedTemplateProv, userTemplateProv) 29 | addCurrentWorkspaceDirsToAggregator(templateProvAgg) 30 | 31 | // Register handlers 32 | registerCommands(context, templateProvAgg) 33 | 34 | // Register workspace change handler for adding/removing template providers 35 | vscode.workspace.onDidChangeWorkspaceFolders((e: vscode.WorkspaceFoldersChangeEvent) => { 36 | e.added.forEach((workspaceDir: vscode.WorkspaceFolder) => { 37 | templateProvAgg.setWorkspaceProvider(workspaceDir.uri, new FileSystemTemplateProvider(vscode.Uri.joinPath(workspaceDir.uri, ".kfftemplates"))) 38 | }) 39 | 40 | e.removed.forEach((workspaceDir: vscode.WorkspaceFolder) => { 41 | templateProvAgg.deleteWorkspaceProvider(workspaceDir.uri) 42 | }) 43 | }) 44 | 45 | // Startup 46 | alertForMissingWPILibExt() 47 | updateGradleRioVersion(updateGradleRIOVer, context) 48 | displayChangelog(showChangelogOnUpdate, context) 49 | } 50 | 51 | /** 52 | * deactivate is run by the VSCode Extension Host at the end of the extension's life cycle. 53 | * It is intended to clean up any remaining resources. 54 | */ 55 | export function deactivate() { } 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "todo.timekeeping.started.format": "YY-MM-DD HH:mm:ss", 12 | "todo.timekeeping.finished.format": "YY-MM-DD HH:mm:ss", 13 | "cSpell.words": [ 14 | "brenek", 15 | "chnglog", 16 | "customfs", 17 | "deployable", 18 | "filegenerator", 19 | "gradlerio", 20 | "jetbrains", 21 | "junit", 22 | "kffshell", 23 | "kotlinforfrc", 24 | "lvuser", 25 | "resettestfolder", 26 | "resettestwindows", 27 | "rio", 28 | "robo", 29 | "roborio", 30 | "SKELE", 31 | "smartdashboard", 32 | "stdlib", 33 | "templateinterpreter", 34 | "templatetype", 35 | "wpilib", 36 | "wpilibj", 37 | "wpilibsuite" 38 | ], 39 | "python.formatting.provider": "black", 40 | "todo.embedded.exclude": [ 41 | "src/template/templates/**", 42 | "src/template/templates.ts", 43 | "**/.*", 44 | "**/.*/**", 45 | "**/_output/**", 46 | "**/bower_components/**", 47 | "**/build/**", 48 | "**/dist/**", 49 | "**/venv/**", 50 | "**/node_modules/**", 51 | "**/out/**", 52 | "**/output/**", 53 | "**/release/**", 54 | "**/releases/**", 55 | "**/static/**", 56 | "**/target/**", 57 | "**/third_party/**", 58 | "**/vendor/**", 59 | "**/CHANGELOG", 60 | "**/CHANGELOG.*", 61 | "**/*.min.*", 62 | "**/*.map", 63 | "**/*.{3ds,3g2,3gp,7z,a,aac,adp,ai,aif,aiff,alz,ape,apk,ar,arj,asf,au,avi,bak,baml,bh,bin,bk,bmp,btif,bz2,bzip2,cab,caf,cgm,class,cmx,cpio,cr2,csv,cur,dat,dcm,deb,dex,djvu,dll,dmg,dng,doc,docm,docx,dot,dotm,dra,DS_Store,dsk,dts,dtshd,dvb,dwg,dxf,ecelp4800,ecelp7470,ecelp9600,egg,eol,eot,epub,exe,f4v,fbs,fh,fla,flac,fli,flv,fpx,fst,fvt,g3,gif,graffle,gz,gzip,h261,h263,h264,icns,ico,ief,img,ipa,iso,jar,jpeg,jpg,jpgv,jpm,jxr,key,ktx,lha,lib,lvp,lz,lzh,lzma,lzo,m3u,m4a,m4v,mar,mdi,mht,mid,midi,mj2,mka,mkv,mmr,mng,mobi,mov,movie,mp3,mp4,mp4a,mpeg,mpg,mpga,mxu,nef,npx,numbers,o,oga,ogg,ogv,otf,pages,pbm,pcx,pdb,pdf,pea,pgm,pic,png,pnm,pot,potm,potx,ppa,ppam,ppm,pps,ppsm,ppsx,ppt,pptm,pptx,psd,pya,pyc,pyo,pyv,qt,rar,ras,raw,resources,rgb,rip,rlc,rmf,rmvb,rtf,rz,s3m,s7z,scpt,sgi,shar,sil,sketch,slk,smv,so,sub,swf,tar,tbz,tbz2,tga,tgz,thmx,tif,tiff,tlz,ttc,ttf,txz,udf,uvh,uvi,uvm,uvp,uvs,uvu,viv,vob,war,wav,wax,wbmp,wdp,weba,webm,webp,whl,wim,wm,wma,wmv,wmx,woff,woff2,wvx,xbm,xif,xla,xlam,xls,xlsb,xlsm,xlsx,xlt,xltm,xltx,xm,xmind,xpi,xpm,xwd,xz,z,zip,zipx}" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { TemplateProviderAggregator, FileSystemTemplateProvider } from "../template/providers"; 3 | 4 | /** 5 | * Sets the isKFFProject context variable which is used for determining if commands should be available or not. 6 | */ 7 | export async function setIsKFFProject() { 8 | vscode.workspace.workspaceFolders?.forEach((workspace: vscode.WorkspaceFolder) => { 9 | vscode.workspace.fs.stat(vscode.Uri.joinPath(workspace.uri, ".wpilib", "wpilib_preferences.json")).then(() => { 10 | vscode.commands.executeCommand("setContext", "isKFFProject", true) 11 | }).then(undefined, _ => { }) 12 | }) 13 | } 14 | 15 | /** 16 | * Checks to see if the WPILib extension is activated and alert the user if it's not. 17 | */ 18 | export async function alertForMissingWPILibExt() { 19 | const wpilibExt = vscode.extensions.getExtension("wpilibsuite.vscode-wpilib"); 20 | 21 | if (wpilibExt === undefined) { 22 | vscode.window.showWarningMessage("Kotlin for FRC is meant to be a companion to the official WPILib extension, but it is not installed or you are in a restricted workspace."); 23 | } 24 | } 25 | 26 | /** 27 | * Get the currently set java home with the gradle syntax prepended. 28 | * 29 | * @returns Gradle arguments that specifically denote a JAVA_HOME 30 | */ 31 | export function getJavaHomeGradleArg(): string { 32 | let javaHomeConfig = vscode.workspace.getConfiguration("java").get("home"); 33 | let kffJavaHomeConfig = vscode.workspace.getConfiguration("kotlinForFRC.simulate").get("javaHome"); 34 | 35 | if ((kffJavaHomeConfig === null || kffJavaHomeConfig === undefined) && (javaHomeConfig === null || javaHomeConfig === undefined)) { 36 | return ""; 37 | } 38 | 39 | let javaHome = ""; 40 | if (kffJavaHomeConfig !== null && kffJavaHomeConfig !== undefined) { 41 | javaHome = kffJavaHomeConfig; 42 | } else if (javaHomeConfig !== null && javaHomeConfig !== undefined) { 43 | // Should just be an else but typescript doesn't realize that the above return statement exists to check null/undefined 44 | javaHome = javaHomeConfig; 45 | } 46 | 47 | return `-Dorg.gradle.java.home="${javaHome}"`; 48 | } 49 | 50 | /** 51 | * addCurrentWorkspaceDirsToAggregator adds each currently open workspace folder to the provided 52 | * template provider aggregator. 53 | * 54 | * @param templateProvAgg The template provider aggregator to add to 55 | */ 56 | export function addCurrentWorkspaceDirsToAggregator(templateProvAgg: TemplateProviderAggregator) { 57 | vscode.workspace.workspaceFolders?.forEach((workspaceDir: vscode.WorkspaceFolder) => { 58 | templateProvAgg.setWorkspaceProvider(workspaceDir.uri, new FileSystemTemplateProvider(vscode.Uri.joinPath(workspaceDir.uri, ".kfftemplates"))) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /src/template/templates/robotBaseRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.hal.DriverStationJNI 4 | import edu.wpi.first.util.WPIUtilJNI 5 | import edu.wpi.first.wpilibj.DriverStation 6 | import edu.wpi.first.wpilibj.RobotBase 7 | import edu.wpi.first.wpilibj.internal.DriverStationModeThread 8 | 9 | /** 10 | * The VM is configured to automatically run this class. If you change the name of this class or the 11 | * package after creating this project, you must also update the build.gradle file in the project. 12 | */ 13 | class Robot : RobotBase() { 14 | fun robotInit() {} 15 | 16 | fun disabled() {} 17 | 18 | fun autonomous() {} 19 | 20 | fun teleop() {} 21 | 22 | fun test() {} 23 | 24 | @Volatile 25 | private var exit = false 26 | 27 | override fun startCompetition() { 28 | robotInit() 29 | 30 | val modeThread = DriverStationModeThread() 31 | val event = WPIUtilJNI.createEvent(false, false) 32 | DriverStation.provideRefreshedDataEventHandle(event) 33 | 34 | // Tell the DS that the robot is ready to be enabled 35 | DriverStationJNI.observeUserProgramStarting() 36 | 37 | while (!Thread.currentThread().isInterrupted && !exit) { 38 | when (true) { 39 | isDisabled -> { 40 | modeThread.inDisabled(true) 41 | disabled() 42 | modeThread.inDisabled(false) 43 | while (isDisabled) { 44 | try { 45 | WPIUtilJNI.waitForObject(event) 46 | } catch (e: InterruptedException) { 47 | Thread.currentThread().interrupt() 48 | } 49 | } 50 | } 51 | isAutonomous -> { 52 | modeThread.inAutonomous(true) 53 | autonomous() 54 | modeThread.inAutonomous(false) 55 | while (isAutonomousEnabled) { 56 | try { 57 | WPIUtilJNI.waitForObject(event) 58 | } catch (e: InterruptedException) { 59 | Thread.currentThread().interrupt() 60 | } 61 | } 62 | } 63 | isTest -> { 64 | modeThread.inTest(true) 65 | test() 66 | modeThread.inTest(false) 67 | while (isTest && isEnabled) { 68 | try { 69 | WPIUtilJNI.waitForObject(event) 70 | } catch (e: InterruptedException) { 71 | Thread.currentThread().interrupt() 72 | } 73 | } 74 | } 75 | else -> { 76 | modeThread.inTeleop(true) 77 | teleop() 78 | modeThread.inTeleop(false) 79 | while (isTeleopEnabled) { 80 | try { 81 | WPIUtilJNI.waitForObject(event) 82 | } catch (e: InterruptedException) { 83 | Thread.currentThread().interrupt() 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | DriverStation.removeRefreshedDataEventHandle(event) 91 | modeThread.close() 92 | } 93 | 94 | override fun endCompetition() { 95 | exit = true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Kotlin for FRC Contributor Guidelines 2 | 3 | This document contains most everything any developer would need to know to start developing Kotlin for FRC, also abbreviated KfF. 4 | 5 | ## Dependencies 6 | 7 | To develop Kotlin for FRC, you will need: 8 | 9 | - [Node.js](https://nodejs.org/en/download) 10 | - NPM \(Included with default Node.js install\) 11 | - [Visual Studio Code](https://code.visualstudio.com/download) 12 | 13 | After installing the above software, clone the repo and run `npm install` inside the kotlin-for-frc directory. 14 | 15 | It is recommended to use Visual Studio Code to develop Kotlin for FRC, because of the deep integration built into VSCode for developing extensions. 16 | 17 | Those who are new to writing VSCode extensions should take a look at the official [API docs](https://code.visualstudio.com/api). 18 | They provide an excellent starting point for understanding the structure of VSCode extensions. 19 | 20 | ## Kotlin for FRC Code Structure 21 | 22 | - `src` 23 | - `commands` - Code related to the commands contributed by KfF. 24 | - `fileManipulation` - Code related to creating and reading files and working with the file system. Mostly just abstractions to make working with the VSCode API a little bit easier to use. 25 | - `templates` - Code related to reading and replacing items in templates. 26 | - `frc-kotlin` - Where the templates are actually stored 27 | - `test` - Unit testing files 28 | - `util` - Miscellaneous utilities grouped into files based on what they do. 29 | - `extension.ts` - The entry point for the extension. Mostly handles startup and teardown logic. 30 | - `constants.ts` - Constants for the project. 31 | - `testing-workspace` - Directory used for unit tests 32 | 33 | ## Repository Structure 34 | 35 | Kotlin for FRC uses a branching model created by Vincent Driessen described on his website [here](https://nvie.com/posts/a-successful-git-branching-model/). 36 | 37 | ### Branches 38 | 39 | - `master` - The main branch of the repository. Should always have the newest stable release. 40 | - `development` - Branch where all features and non-critical bugfixes are kept until the next release. 41 | - release - Staging branches. Only bugfixes and stability improvements are committed to these branches. 42 | - feature branches - Feature branches are created by contributors on their own forks and machines. They stem from `development` and merge back into `development` when the feature is complete. 43 | - bugfix branches - Similar to feature branches but they are often very small and merged very quickly. They fix any bugs that are in `develop` or `master`. 44 | - hotfix branches - Similar to bugfix branches but they are very severe issues. They typically stem from the `master` and are merged back into `master` as well as into `development`. 45 | 46 | > Note: Severe issues are categorized by security holes or crashing extensions. Other bugs may be included in this definition if the need arises. 47 | 48 | ## Submitting a change 49 | 50 | The first step to submitting a change to Kotlin for FRC, is to open up an issue on GitHub discussing what you want changed. 51 | You should express your interest in making the change yourself if you so desire. 52 | Creating an issue allows others to comment and help improve the ideas and changes that are being proposed. 53 | 54 | Once your change is approved and the code is complete, you will need to submit a pull request. 55 | A tutorial detailing how to contribute using the fork/pull request model can be found [here](https://reflectoring.io/github-fork-and-pull/). 56 | 57 | Kotlin for FRC uses GitHub Actions and LGTM Code Analysis to ensure that pull requests contribute quality code. 58 | Any pull request will not be merged until these tools pass on the proposed code. 59 | -------------------------------------------------------------------------------- /src/template/templates/commandRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj.TimedRobot 4 | import edu.wpi.first.wpilibj2.command.Command 5 | import edu.wpi.first.wpilibj2.command.CommandScheduler 6 | 7 | /** 8 | * The VM is configured to automatically run this class, and to call the functions corresponding to 9 | * each mode, as described in the TimedRobot documentation. If you change the name of this class or 10 | * the package after creating this project, you must also update the build.gradle file in the 11 | * project. 12 | */ 13 | class Robot : TimedRobot() { 14 | private var autonomousCommand: Command? = null 15 | private var robotContainer: RobotContainer? = null 16 | 17 | /** 18 | * This function is run when the robot is first started up and should be used for any 19 | * initialization code. 20 | */ 21 | override fun robotInit() { 22 | // Instantiate our RobotContainer. This will perform all our button bindings, and put our 23 | // autonomous chooser on the dashboard. 24 | robotContainer = RobotContainer() 25 | } 26 | 27 | /** 28 | * This function is called every 20 ms, no matter the mode. Use this for items like diagnostics 29 | * that you want ran during disabled, autonomous, teleoperated and test. 30 | * 31 | * 32 | * This runs after the mode specific periodic functions, but before LiveWindow and 33 | * SmartDashboard integrated updating. 34 | */ 35 | override fun robotPeriodic() { 36 | // Runs the Scheduler. This is responsible for polling buttons, adding newly-scheduled 37 | // commands, running already-scheduled commands, removing finished or interrupted commands, 38 | // and running subsystem periodic() methods. This must be called from the robot's periodic 39 | // block in order for anything in the Command-based framework to work. 40 | CommandScheduler.getInstance().run() 41 | } 42 | 43 | /** This function is called once each time the robot enters Disabled mode. */ 44 | override fun disabledInit() {} 45 | 46 | /** This function is called periodically when disabled. */ 47 | override fun disabledPeriodic() {} 48 | 49 | /** This autonomous runs the autonomous command selected by your [RobotContainer] class. */ 50 | override fun autonomousInit() { 51 | autonomousCommand = robotContainer?.autonomousCommand 52 | 53 | // Schedule the autonomous command (example) 54 | // Note the Kotlin safe-call(?.), this ensures autonomousCommand is not null before scheduling it 55 | autonomousCommand?.schedule() 56 | } 57 | 58 | /** This function is called periodically during autonomous. */ 59 | override fun autonomousPeriodic() {} 60 | 61 | /** This function is called once when teleop is enabled. */ 62 | override fun teleopInit() { 63 | // This makes sure that the autonomous stops running when 64 | // teleop starts running. If you want the autonomous to 65 | // continue until interrupted by another command, remove 66 | // this line or comment it out. 67 | // Note the Kotlin safe-call(?.), this ensures autonomousCommand is not null before cancelling it 68 | autonomousCommand?.cancel() 69 | } 70 | 71 | /** This function is called periodically during operator control. */ 72 | override fun teleopPeriodic() {} 73 | 74 | /** This function is called once when test mode is enabled. */ 75 | override fun testInit() { 76 | // Cancels all running commands at the start of test mode. 77 | CommandScheduler.getInstance().cancelAll() 78 | } 79 | 80 | /** This function is called periodically during test mode. */ 81 | override fun testPeriodic() {} 82 | 83 | /** This function is called once when the robot is first started up. */ 84 | override fun simulationInit() {} 85 | 86 | /** This function is called periodically whilst in simulation. */ 87 | override fun simulationPeriodic() {} 88 | } 89 | -------------------------------------------------------------------------------- /src/template/templates/romiTimedRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj.TimedRobot 4 | import edu.wpi.first.wpilibj.smartdashboard.SendableChooser 5 | import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard 6 | 7 | /** 8 | * The VM is configured to automatically run this class, and to call the 9 | * functions corresponding to each mode, as described in the TimedRobot 10 | * documentation. If you change the name of this class or the package after 11 | * creating this project, you must also update the build.gradle file in the 12 | * project. 13 | */ 14 | class Robot : TimedRobot() { 15 | private var selectedAutonomous: String? = null 16 | private val autonomousChooser = SendableChooser() 17 | private val drivetrain = RomiDrivetrain() 18 | 19 | companion object { 20 | private const val DEFAULT_AUTO = "Default" 21 | private const val CUSTOM_AUTO = "My Auto" 22 | } 23 | 24 | /** 25 | * This function is run when the robot is first started up and should be 26 | * used for any initialization code. 27 | */ 28 | override fun robotInit() { 29 | autonomousChooser.setDefaultOption("Default Auto", DEFAULT_AUTO) 30 | autonomousChooser.addOption("My Auto", CUSTOM_AUTO) 31 | SmartDashboard.putData("Auto choices", autonomousChooser) 32 | } 33 | 34 | /** 35 | * This function is called every 20 ms, no matter the mode. Use this for 36 | * items like diagnostics that you want ran during disabled, autonomous, 37 | * teleoperated and test. 38 | * 39 | * 40 | * This runs after the mode specific periodic functions, but before 41 | * LiveWindow and SmartDashboard integrated updating. 42 | */ 43 | override fun robotPeriodic() {} 44 | 45 | /** 46 | * This autonomous (along with the chooser code above) shows how to select 47 | * between different autonomous modes using the dashboard. The sendable 48 | * chooser code works with the Java SmartDashboard. If you prefer the 49 | * LabVIEW Dashboard, remove all of the chooser code and uncomment the 50 | * getString line to get the auto name from the text box below the Gyro 51 | * 52 | * 53 | * You can add additional auto modes by adding additional comparisons to 54 | * the switch structure below with additional strings. If using the 55 | * SendableChooser make sure to add them to the chooser code above as well. 56 | */ 57 | override fun autonomousInit() { 58 | selectedAutonomous = autonomousChooser.selected 59 | // m_autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto); 60 | println("Auto selected: $selectedAutonomous") 61 | drivetrain.resetEncoders() 62 | } 63 | 64 | /** 65 | * This function is called periodically during autonomous. 66 | */ 67 | override fun autonomousPeriodic() { 68 | when (selectedAutonomous) { 69 | CUSTOM_AUTO -> { 70 | } 71 | DEFAULT_AUTO -> { 72 | } 73 | else -> { 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * This function is called once when teleop is enabled. 80 | */ 81 | override fun teleopInit() {} 82 | 83 | /** 84 | * This function is called periodically during operator control. 85 | */ 86 | override fun teleopPeriodic() {} 87 | 88 | /** 89 | * This function is called once when the robot is disabled. 90 | */ 91 | override fun disabledInit() {} 92 | 93 | /** 94 | * This function is called periodically when disabled. 95 | */ 96 | override fun disabledPeriodic() {} 97 | 98 | /** 99 | * This function is called once when test mode is enabled. 100 | */ 101 | override fun testInit() {} 102 | 103 | /** 104 | * This function is called periodically during test mode. 105 | */ 106 | override fun testPeriodic() {} 107 | } 108 | -------------------------------------------------------------------------------- /src/template/templates/timedRobot.kfftemplate: -------------------------------------------------------------------------------- 1 | package frc.robot 2 | 3 | import edu.wpi.first.wpilibj.TimedRobot 4 | import edu.wpi.first.wpilibj.smartdashboard.SendableChooser 5 | import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard 6 | 7 | /** 8 | * The VM is configured to automatically run this class, and to call the 9 | * functions corresponding to each mode, as described in the TimedRobot 10 | * documentation. If you change the name of this class or the package after 11 | * creating this project, you must also update the build.gradle file in the 12 | * project. 13 | */ 14 | class Robot : TimedRobot() { 15 | private var selectedAutonomous: String? = null 16 | private val autonomousChooser = SendableChooser() 17 | 18 | companion object { 19 | private const val DEFAULT_AUTO = "Default" 20 | private const val CUSTOM_AUTO = "My Auto" 21 | } 22 | 23 | /** 24 | * This function is run when the robot is first started up and should be 25 | * used for any initialization code. 26 | */ 27 | override fun robotInit() { 28 | autonomousChooser.setDefaultOption("Default Auto", DEFAULT_AUTO) 29 | autonomousChooser.addOption("My Auto", CUSTOM_AUTO) 30 | SmartDashboard.putData("Auto choices", autonomousChooser) 31 | } 32 | 33 | /** 34 | * This function is called every 20 ms, no matter the mode. Use this for 35 | * items like diagnostics that you want ran during disabled, autonomous, 36 | * teleoperated and test. 37 | * 38 | * 39 | * This runs after the mode specific periodic functions, but before 40 | * LiveWindow and SmartDashboard integrated updating. 41 | */ 42 | override fun robotPeriodic() {} 43 | 44 | /** 45 | * This autonomous (along with the chooser code above) shows how to select 46 | * between different autonomous modes using the dashboard. The sendable 47 | * chooser code works with the Java SmartDashboard. If you prefer the 48 | * LabVIEW Dashboard, remove all of the chooser code and uncomment the 49 | * getString line to get the auto name from the text box below the Gyro 50 | * 51 | * 52 | * You can add additional auto modes by adding additional comparisons to 53 | * the switch structure below with additional strings. If using the 54 | * SendableChooser make sure to add them to the chooser code above as well. 55 | */ 56 | override fun autonomousInit() { 57 | selectedAutonomous = autonomousChooser.selected 58 | // autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto) 59 | println("Auto selected: $selectedAutonomous") 60 | } 61 | 62 | /** 63 | * This function is called periodically during autonomous. 64 | */ 65 | override fun autonomousPeriodic() { 66 | when (selectedAutonomous) { 67 | CUSTOM_AUTO -> { 68 | } 69 | DEFAULT_AUTO -> { 70 | } 71 | else -> { 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * This function is called once when teleop is enabled. 78 | */ 79 | override fun teleopInit() {} 80 | 81 | /** 82 | * This function is called periodically during operator control. 83 | */ 84 | override fun teleopPeriodic() {} 85 | 86 | /** 87 | * This function is called once when the robot is disabled. 88 | */ 89 | override fun disabledInit() {} 90 | 91 | /** 92 | * This function is called periodically when disabled. 93 | */ 94 | override fun disabledPeriodic() {} 95 | 96 | /** 97 | * This function is called once when test mode is enabled. 98 | */ 99 | override fun testInit() {} 100 | 101 | /** 102 | * This function is called periodically during test mode. 103 | */ 104 | override fun testPeriodic() {} 105 | 106 | /** This function is called once when the robot is first started up. */ 107 | override fun simulationInit() {} 108 | 109 | /** This function is called periodically whilst in simulation. */ 110 | override fun simulationPeriodic() {} 111 | } 112 | -------------------------------------------------------------------------------- /src/test/suite/template/providers.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import { Uri } from "vscode" 3 | import { ITemplateProvider, TemplateType } from "../../../template/models" 4 | import { TemplateProviderAggregator } from "../../../template/providers" 5 | 6 | suite("TemplateProviderAggregator", function () { 7 | test("getTemplate calls all sub-providers", async function () { 8 | const integrated = new TestTemplateProvider() 9 | const user = new TestTemplateProvider() 10 | const workspace = new TestTemplateProvider() 11 | 12 | const aggregator = new TemplateProviderAggregator(integrated, user) 13 | aggregator.setWorkspaceProvider(Uri.file("/test/file"), workspace) 14 | 15 | await aggregator.getTemplate(TemplateType.emptyClass, Uri.file("/test/file")) 16 | 17 | assert.strictEqual(integrated.getTemplateCalled, true, "Integrated template provider not called.") 18 | assert.strictEqual(user.getTemplateCalled, true, "User template provider not called.") 19 | assert.strictEqual(workspace.getTemplateCalled, true, "Workspace template provider not called.") 20 | }) 21 | 22 | test("getTemplate passes arguments to each sub-provider", async function () { 23 | const integrated = new TestTemplateProvider() 24 | const user = new TestTemplateProvider() 25 | const workspace = new TestTemplateProvider() 26 | 27 | const aggregator = new TemplateProviderAggregator(integrated, user) 28 | aggregator.setWorkspaceProvider(Uri.file("/test/file"), workspace) 29 | 30 | const wrkspceFolder = Uri.file("/test/file") 31 | await aggregator.getTemplate(TemplateType.emptyClass, wrkspceFolder) 32 | 33 | assert.strictEqual(integrated.templateTypeArg, TemplateType.emptyClass, "Integrated template provider called with incorrect templateTypeArg.") 34 | assert.strictEqual(integrated.workspaceFolderArg, wrkspceFolder, "Integrated template provider called with incorrect workspaceFolderArg.") 35 | 36 | assert.strictEqual(user.templateTypeArg, TemplateType.emptyClass, "User template provider called with incorrect templateTypeArg.") 37 | assert.strictEqual(user.workspaceFolderArg, wrkspceFolder, "User template provider called with incorrect workspaceFolderArg.") 38 | 39 | assert.strictEqual(workspace.templateTypeArg, TemplateType.emptyClass, "Workspace template provider called with incorrect templateTypeArg.") 40 | assert.strictEqual(workspace.workspaceFolderArg, wrkspceFolder, "Workspace template provider called with incorrect workspaceFolderArg.") 41 | }) 42 | 43 | test("getTemplate returns early when a provider gives a non-null value", async function () { 44 | const integrated = new TestTemplateProvider() 45 | const user = new TestTemplateProvider() 46 | const workspace = new TestTemplateProvider("") 47 | 48 | const aggregator = new TemplateProviderAggregator(integrated, user) 49 | aggregator.setWorkspaceProvider(Uri.file("/test/file"), workspace) 50 | 51 | await aggregator.getTemplate(TemplateType.emptyClass, Uri.file("/test/file")) 52 | 53 | assert.strictEqual(workspace.getTemplateCalled, true) 54 | assert.strictEqual(user.getTemplateCalled, false) 55 | assert.strictEqual(integrated.getTemplateCalled, false) 56 | 57 | workspace.returnValue = null 58 | user.returnValue = "" 59 | await aggregator.getTemplate(TemplateType.emptyClass, Uri.file("/test/file")) 60 | 61 | assert.strictEqual(workspace.getTemplateCalled, true) 62 | assert.strictEqual(user.getTemplateCalled, true) 63 | assert.strictEqual(integrated.getTemplateCalled, false) 64 | }) 65 | }) 66 | 67 | /** 68 | * TestTemplateProvider is a mock template provider whose return value for getTemplate 69 | * can be manipulated. 70 | */ 71 | class TestTemplateProvider implements ITemplateProvider { 72 | getTemplateCalled = false 73 | templateTypeArg: TemplateType | undefined = undefined 74 | workspaceFolderArg: Uri | undefined = undefined 75 | returnValue: string | null 76 | 77 | constructor(returnValue?: string | null) { 78 | this.returnValue = returnValue !== undefined ? returnValue : null 79 | } 80 | 81 | async getTemplate(t: TemplateType, workspaceFolder: Uri): Promise { 82 | this.getTemplateCalled = true 83 | this.templateTypeArg = t 84 | this.workspaceFolderArg = workspaceFolder 85 | return this.returnValue 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/template/templates/buildGradle.kfftemplate: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "edu.wpi.first.GradleRIO" version "#{GRADLE_RIO_VERSION}" 4 | id "org.jetbrains.kotlin.jvm" version '1.8.0' 5 | } 6 | 7 | sourceCompatibility = JavaVersion.VERSION_11 8 | targetCompatibility = JavaVersion.VERSION_11 9 | 10 | def ROBOT_MAIN_CLASS = "frc.robot.Main" 11 | 12 | // Define my targets (RoboRIO) and artifacts (deployable files) 13 | // This is added by GradleRIO's backing project DeployUtils. 14 | deploy { 15 | targets { 16 | roborio(getTargetTypeClass('RoboRIO')) { 17 | // Team number is loaded either from the .wpilib/wpilib_preferences.json 18 | // or from command line. If not found an exception will be thrown. 19 | // You can use getTeamOrDefault(team) instead of getTeamNumber if you 20 | // want to store a team number in this file. 21 | team = project.frc.getTeamNumber() 22 | debug = project.frc.getDebugOrDefault(false) 23 | 24 | artifacts { 25 | // First part is artifact name, 2nd is artifact type 26 | // getTargetTypeClass is a shortcut to get the class type using a string 27 | 28 | frcJava(getArtifactTypeClass('FRCJavaArtifact')) { 29 | } 30 | 31 | // Static files artifact 32 | frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { 33 | files = project.fileTree('src/main/deploy') 34 | directory = '/home/lvuser/deploy' 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | def deployArtifact = deploy.targets.roborio.artifacts.frcJava 42 | 43 | // Set to true to use debug for JNI. 44 | wpi.java.debugJni = false 45 | 46 | // Set this to true to enable desktop support. 47 | def includeDesktopSupport = false 48 | 49 | // Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. 50 | // Also defines JUnit 5. 51 | dependencies { 52 | implementation wpi.java.deps.wpilib() 53 | implementation wpi.java.vendor.java() 54 | 55 | roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) 56 | roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) 57 | 58 | roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) 59 | roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) 60 | 61 | nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) 62 | nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) 63 | simulationDebug wpi.sim.enableDebug() 64 | 65 | nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) 66 | nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) 67 | simulationRelease wpi.sim.enableRelease() 68 | 69 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' 70 | testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.2' 71 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' 72 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 73 | } 74 | 75 | test { 76 | useJUnitPlatform() 77 | systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' 78 | } 79 | 80 | // Simulation configuration (e.g. environment variables). 81 | wpi.sim.addGui().defaultEnabled = true 82 | wpi.sim.addDriverstation() 83 | 84 | // Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') 85 | // in order to make them all available at runtime. Also adding the manifest so WPILib 86 | // knows where to look for our Robot Class. 87 | jar { 88 | from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } 89 | manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) 90 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 91 | } 92 | 93 | // Configure jar and deploy tasks 94 | deployArtifact.jarTask = jar 95 | wpi.java.configureExecutableTasks(jar) 96 | wpi.java.configureTestTasks(test) 97 | 98 | // Configure string concat to always inline compile 99 | tasks.withType(JavaCompile) { 100 | options.compilerArgs.add '-XDstringConcat=inline' 101 | } 102 | repositories { 103 | mavenCentral() 104 | } 105 | 106 | compileKotlin { 107 | kotlinOptions { 108 | jvmTarget = targetCompatibility 109 | } 110 | } 111 | 112 | compileTestKotlin { 113 | kotlinOptions { 114 | jvmTarget = targetCompatibility 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/template/models.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | /** 4 | * ITemplateProvider defines how the template engine expects a template provider to behave. 5 | */ 6 | export interface ITemplateProvider { 7 | /** 8 | * getTemplate returns the raw text of the requested template type. 9 | * 10 | * @param t The type of template to use 11 | * @param workspaceFolder The workspace folder to extract workspace-level templates from 12 | */ 13 | getTemplate(t: TemplateType, workspaceFolder: vscode.Uri): Promise 14 | } 15 | 16 | /** 17 | * The TemplateType enum defines the different types of templates that KfF can generate. 18 | */ 19 | export enum TemplateType { 20 | // Command based templates 21 | // General 22 | commandRobot = "Command Based Robot", 23 | commandConstants = "Constants", 24 | robotContainer = "Robot Container", 25 | // Commands 26 | command = "Command", 27 | instantCommand = "Instant Command", 28 | parallelCommandGroup = "Parallel Command Group", 29 | parallelDeadlineGroup = "Parallel Deadline Group", 30 | parallelRaceGroup = "Parallel Race Group", 31 | PIDCommand = "PID Command", 32 | profiledPIDCommand = "Profiled PID Command", 33 | sequentialCommandGroup = "Sequential Command Group", 34 | trapezoidProfileCommand = "Trapezoid Profile Command", 35 | commandExampleCommand = "Example Command", 36 | commandAutos = "Autos", 37 | // Subsystems 38 | subsystem = "Subsystem", 39 | exampleSubsystem = "Example Subsystem", 40 | PIDSubsystem = "PID Subsystem", 41 | profiledPIDSubsystem = "Profiled PID Subsystem", 42 | trapezoidProfileSubsystem = "Trapezoid Profile Subsystem", 43 | // Skeleton 44 | commandSkeletonRobot = "Command Based Skeleton Robot", 45 | commandSkeletonRobotContainer = "Command Based Skeleton Robot Container", 46 | 47 | 48 | // Romi 49 | romiTimedRobot = "Romi Timed Robot", 50 | romiTimedDrivetrain = "Romi Timed Drivetrain", 51 | romiCommandRobotContainer = "Romi Command Robot Container", 52 | romiCommandConstants = "Romi Command Constants", 53 | romiCommandExampleCommand = "Romi Command Example Command", 54 | romiCommandDrivetrainSubsystem = "Romi Command Drivetrain Subsystem", 55 | 56 | // Robot.kt files 57 | robotBaseRobot = "Robot Base", 58 | timedRobot = "Timed Robot", 59 | timedSkeleton = "Timed Skeleton", 60 | 61 | // Misc templates 62 | main = "Main.kt", 63 | emptyClass = "Empty Class", 64 | buildGradle = "build.gradle", 65 | romiBuildGradle = "Romi-specific build.gradle", 66 | } 67 | 68 | /** 69 | * parseStringToTemplateType maps a string to a TemplateType. 70 | * 71 | * If the string can't be mapped, an error is thrown. 72 | * 73 | * @param input String to parse 74 | * @returns The resulting template type 75 | */ 76 | export function parseStringToTemplateType(input: string): TemplateType { 77 | switch (input) { 78 | // Command based 79 | // General 80 | case "commandRobot": 81 | return TemplateType.commandRobot 82 | case "robotContainer": 83 | return TemplateType.robotContainer 84 | case "commandConstants": 85 | return TemplateType.commandConstants 86 | // Commands 87 | case "command": 88 | return TemplateType.command 89 | case "instantCommand": 90 | return TemplateType.instantCommand 91 | case "parallelCommandGroup": 92 | return TemplateType.parallelCommandGroup 93 | case "parallelDeadlineGroup": 94 | return TemplateType.parallelDeadlineGroup 95 | case "parallelRaceGroup": 96 | return TemplateType.parallelRaceGroup 97 | case "PIDCommand": 98 | return TemplateType.PIDCommand 99 | case "profiledPIDCommand": 100 | return TemplateType.profiledPIDCommand 101 | case "sequentialCommandGroup": 102 | return TemplateType.sequentialCommandGroup 103 | case "trapezoidProfileCommand": 104 | return TemplateType.trapezoidProfileCommand 105 | // Subsystems 106 | case "subsystem": 107 | return TemplateType.subsystem 108 | case "PIDSubsystem": 109 | return TemplateType.PIDSubsystem 110 | case "profiledPIDSubsystem": 111 | return TemplateType.profiledPIDSubsystem 112 | case "trapezoidProfileSubsystem": 113 | return TemplateType.trapezoidProfileSubsystem 114 | // Skeleton 115 | case "commandSkeletonRobot": 116 | return TemplateType.commandSkeletonRobot 117 | case "commandSkeletonRobotContainer": 118 | return TemplateType.commandSkeletonRobotContainer 119 | 120 | // Misc 121 | case "buildGradle": 122 | return TemplateType.buildGradle 123 | case "emptyClass": 124 | return TemplateType.emptyClass 125 | 126 | default: 127 | throw new Error("Invalid string passed to parseStringToTemplateType") 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /images/sources/wpilib-edittable.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 21 | 38 | 39 | 45 | 49 | 54 | 55 | 56 | 61 | 65 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kotlin-for-frc", 3 | "displayName": "Kotlin for FRC", 4 | "description": "Kotlin support for WPILib, unofficially", 5 | "version": "2024.1.1", 6 | "publisher": "Brenek", 7 | "engines": { 8 | "vscode": "^1.66.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "icon": "images/wpilib-with-kotlin_K-128x128.png", 14 | "activationEvents": [ 15 | "workspaceContains:.wpilib/wpilib_preferences.json", 16 | "onCommand:kotlinForFRC.createNew", 17 | "onCommand:kotlinForFRC.convertJavaProject", 18 | "onCommand:kotlinForFRC.showChangelog", 19 | "onCommand:kotlinForFRC.resetAutoShowChangelog", 20 | "onCommand:kotlinForFRC.updateGradleRIOVersion", 21 | "onCommand:kotlinForFRC.resetGradleRIOCache", 22 | "onCommand:kotlinForFRC.simulateFRCKotlinCode", 23 | "onCommand:workbench.action.tasks.runTask" 24 | ], 25 | "main": "./dist/extension", 26 | "capabilities": { 27 | "untrustedWorkspaces": { 28 | "supported": "limited", 29 | "description": "While most of Kotlin for FRC's functionality is essentially text manipulation, it does provide a convenience feature for running the FRC simulation against Kotlin code which requires the workspace to be trusted." 30 | }, 31 | "virtualWorkspaces": false 32 | }, 33 | "contributes": { 34 | "configuration": { 35 | "title": "Kotlin for FRC", 36 | "properties": { 37 | "kotlinForFRC.gradleRioVersion.autoUpdate": { 38 | "type": "boolean", 39 | "default": true, 40 | "scope": "window", 41 | "description": "Whether or not to automatically update the Gradle RIO Version when the Kotlin for FRC extension is loaded." 42 | }, 43 | "kotlinForFRC.changelog.showOnUpdate": { 44 | "type": "boolean", 45 | "default": true, 46 | "scope": "application", 47 | "description": "Whether or not to show the changelog when Kotlin for FRC has been updated." 48 | }, 49 | "kotlinForFRC.simulate.javaHome": { 50 | "type": "string", 51 | "default": null, 52 | "scope": "window", 53 | "description": "Defines a custom JAVA_HOME for Kotlin for FRC to use when simulating FRC Kotlin code." 54 | } 55 | } 56 | }, 57 | "commands": [ 58 | { 59 | "command": "kotlinForFRC.convertJavaProject", 60 | "title": "Convert Java Project to Kotlin", 61 | "category": "Kotlin-FRC" 62 | }, 63 | { 64 | "command": "kotlinForFRC.createNew", 65 | "title": "Create new Kotlin class/command here", 66 | "category": "Kotlin-FRC" 67 | }, 68 | { 69 | "command": "kotlinForFRC.showChangelog", 70 | "title": "Show Changelog", 71 | "category": "Kotlin-FRC" 72 | }, 73 | { 74 | "command": "kotlinForFRC.resetAutoShowChangelog", 75 | "title": "Reset Auto-Show Changelog Cache", 76 | "category": "Kotlin-FRC" 77 | }, 78 | { 79 | "command": "kotlinForFRC.updateGradleRIOVersion", 80 | "title": "Update GradleRIO Version", 81 | "category": "Kotlin-FRC" 82 | }, 83 | { 84 | "command": "kotlinForFRC.resetGradleRIOCache", 85 | "title": "Reset GradleRIO Version Cache", 86 | "category": "Kotlin-FRC" 87 | }, 88 | { 89 | "command": "kotlinForFRC.simulateFRCKotlinCode", 90 | "title": "Simulate FRC Kotlin Code", 91 | "category": "Kotlin-FRC" 92 | } 93 | ], 94 | "menus": { 95 | "explorer/context": [ 96 | { 97 | "when": "explorerResourceIsFolder && isKFFProject", 98 | "command": "kotlinForFRC.createNew", 99 | "group": "frc-kotlin" 100 | } 101 | ], 102 | "commandPalette": [ 103 | { 104 | "command": "kotlinForFRC.convertJavaProject", 105 | "when": "isKFFProject" 106 | }, 107 | { 108 | "command": "kotlinForFRC.showChangelog" 109 | }, 110 | { 111 | "command": "kotlinForFRC.resetAutoShowChangelog", 112 | "when": "isKFFProject" 113 | }, 114 | { 115 | "command": "kotlinForFRC.updateGradleRIOVersion", 116 | "when": "isKFFProject" 117 | }, 118 | { 119 | "command": "kotlinForFRC.simulateFRCKotlinCode", 120 | "when": "isWorkspaceTrusted && isKFFProject" 121 | }, 122 | { 123 | "command": "kotlinForFRC.resetGradleRIOCache" 124 | } 125 | ] 126 | }, 127 | "taskDefinitions": [ 128 | { 129 | "type": "kffshell" 130 | } 131 | ] 132 | }, 133 | "scripts": { 134 | "vscode:prepublish": "webpack --mode production", 135 | "compile": "webpack --mode none", 136 | "watch": "webpack --mode none --watch", 137 | "test-compile": "tsc -p ./", 138 | "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", 139 | "pretest": "npm run test-compile && npm run lint", 140 | "test": "npm run test-compile && node ./out/test/runTest.js" 141 | }, 142 | "devDependencies": { 143 | "@types/mocha": "^10.0.10", 144 | "@types/node": "^20.14.8", 145 | "@types/semver": "^7.5.8", 146 | "@types/vscode": "1.66.0", 147 | "@typescript-eslint/eslint-plugin": "^8.24.0", 148 | "@typescript-eslint/parser": "^8.24.0", 149 | "@vscode/test-electron": "^2.4.1", 150 | "acorn": "^8.14.0", 151 | "cross-spawn": "^7.0.6", 152 | "eslint": "^9.20.1", 153 | "glob": "^11.0.1", 154 | "mocha": "^11.1.0", 155 | "ts-loader": "^9.5.2", 156 | "typescript": "^5.7.3", 157 | "webpack": "^5.98.0", 158 | "webpack-cli": "^6.0.1" 159 | }, 160 | "dependencies": { 161 | "axios": "^1.12.0", 162 | "semver": "^7.7.1" 163 | }, 164 | "extensionDependencies": [], 165 | "repository": { 166 | "type": "git", 167 | "url": "https://github.com/BrenekH/kotlin-for-frc.git" 168 | }, 169 | "license": "MIT" 170 | } 171 | -------------------------------------------------------------------------------- /.github/workflows/node_js.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [20.x, 22.x, 23.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Cache Node Modules 22 | uses: actions/cache@v4 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | 29 | - name: Install Frozen Dependencies 30 | run: npm ci 31 | 32 | - run: xvfb-run -a npm test 33 | 34 | build: 35 | runs-on: ubuntu-latest 36 | 37 | strategy: 38 | matrix: 39 | node-version: [20.x, 22.x, 23.x] 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | 49 | - name: Cache Node Modules 50 | uses: actions/cache@v4 51 | env: 52 | cache-name: cache-node-modules 53 | with: 54 | path: ~/.npm 55 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 56 | 57 | - name: Globally Install VSCE 58 | run: npm install -g vsce 59 | 60 | - name: Install Frozen Dependencies 61 | run: npm ci 62 | 63 | - run: vsce package 64 | 65 | - name: Upload .vsix artifact 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: vsix-file 69 | path: "*.vsix" 70 | if: startsWith(github.ref, 'refs/tags/') 71 | 72 | deploy-github-releases: 73 | runs-on: ubuntu-latest 74 | 75 | needs: [build, test] 76 | if: success() && startsWith(github.ref, 'refs/tags/') 77 | 78 | steps: 79 | - name: Checkout Code 80 | uses: actions/checkout@v4 81 | 82 | - name: Download .vsix artifact 83 | uses: actions/download-artifact@v4 84 | with: 85 | name: vsix-file 86 | 87 | - name: Set PREREL environment variable 88 | env: 89 | TAG: ${{ github.ref }} 90 | run: echo "PREREL=$(if [[ $TAG =~ "alpha" ]] || [[ $TAG =~ "beta" ]] || [[ $TAG =~ "rc" ]]; then echo "true"; else echo "false"; fi;)" >> $GITHUB_ENV 91 | 92 | - name: Create Modified Release Notes 93 | run: sed '1,4d' RELEASE_NOTES.md > modded_release_notes.md 94 | 95 | - name: Create sanitized github.ref 96 | run: echo "TAG_USED=$(echo ${GITHUB_REF:10})" >> $GITHUB_ENV 97 | 98 | - name: Create Release with Assets 99 | uses: softprops/action-gh-release@v2 100 | with: 101 | name: Version ${{ env.TAG_USED }} 102 | draft: false 103 | prerelease: ${{ env.PREREL }} 104 | files: ./kotlin-for-frc-${{ env.TAG_USED }}.vsix 105 | body_path: ./modded_release_notes.md 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | 109 | deploy-vscode-marketplace: 110 | runs-on: ubuntu-latest 111 | 112 | needs: [build, test] 113 | if: success() && startsWith(github.ref, 'refs/tags/') 114 | 115 | steps: 116 | - uses: actions/checkout@v4 117 | 118 | - name: Setup Node.js v20.x 119 | uses: actions/setup-node@v4 120 | with: 121 | node-version: 20.x 122 | 123 | - name: Set PACKAGE_TAG environment variable 124 | run: echo "PACKAGE_TAG=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 125 | 126 | - name: Cache Node Modules 127 | uses: actions/cache@v4 128 | env: 129 | cache-name: cache-node-modules 130 | with: 131 | path: ~/.npm 132 | key: ${{ runner.os }}-node20.x-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 133 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 134 | 135 | - name: Globally Install VSCE 136 | run: npm install -g vsce 137 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 138 | 139 | - name: Install Frozen Dependencies 140 | run: npm ci 141 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 142 | 143 | - name: Publish to VSCode Marketplace 144 | run: vsce publish -p ${{ secrets.VSCE_PUBLISHER_TOKEN }} 145 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 146 | 147 | deploy-open-vsx: 148 | runs-on: ubuntu-latest 149 | 150 | needs: [build, test] 151 | if: success() && startsWith(github.ref, 'refs/tags/') 152 | 153 | steps: 154 | - uses: actions/checkout@v4 155 | 156 | - name: Setup Node.js v20.x 157 | uses: actions/setup-node@v4 158 | with: 159 | node-version: 20.x 160 | 161 | - name: Set PACKAGE_TAG environment variable 162 | run: echo "PACKAGE_TAG=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 163 | 164 | - name: Cache Node Modules 165 | uses: actions/cache@v4 166 | env: 167 | cache-name: cache-node-modules 168 | with: 169 | path: ~/.npm 170 | key: ${{ runner.os }}-node20.x-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 171 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 172 | 173 | - name: Globally Install OVSX 174 | run: npm install -g ovsx 175 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 176 | 177 | - name: Install Frozen Dependencies 178 | run: npm ci 179 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 180 | 181 | - name: Publish to Open VSX 182 | run: ovsx publish -p ${{ secrets.OVSX_PUBLISHER_TOKEN }} 183 | if: "!(contains(env.PACKAGE_TAG, 'alpha') && !contains(env.PACKAGE_TAG, 'beta') && !contains(env.PACKAGE_TAG, 'rc'))" 184 | -------------------------------------------------------------------------------- /images/sources/kff-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 76 | 78 | 79 | 87 | 91 | 95 | 99 | 100 | 105 | 106 | 111 | -------------------------------------------------------------------------------- /src/util/gradleRioUpdate.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver" 2 | import * as vscode from "vscode" 3 | import axios from "axios" 4 | import { TARGET_GRADLE_RIO_YEAR } from "../constants" 5 | 6 | /** 7 | * updateGradleRioVersion updates the GradleRIO version in any open workspace folder which is suspected to be a converted 8 | * Kotlin-FRC project. 9 | * 10 | * @param autoUpdateEnabled Whether or not the function should update the GradleRIO version 11 | * @param context VSCode extension context 12 | */ 13 | export default async function updateGradleRioVersion(autoUpdateEnabled: boolean, context: vscode.ExtensionContext) { 14 | if (!autoUpdateEnabled) { return } 15 | 16 | vscode.workspace.workspaceFolders?.forEach(async (workspaceDir: vscode.WorkspaceFolder) => { 17 | // Ignore workspace folder if it doesn't have a Robot.kt file 18 | const robotKt = vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt") 19 | try { 20 | await vscode.workspace.fs.stat(robotKt) 21 | } catch (_) { 22 | return 23 | } 24 | 25 | // Get current version from workspaceDir/build.gradle 26 | const localVer = await getGradleRIOVersionFromWorkspace(workspaceDir) 27 | if (localVer === null) { 28 | vscode.window.showErrorMessage(`Unable to determine current GradleRIO version in ${workspaceDir.uri.toString()} workspace folder`) 29 | return 30 | } 31 | 32 | const currentYear = semver.parse(localVer)?.major as string | undefined 33 | 34 | // Get latest version from plugins.gradle.org 35 | const latestVer = await getLatestGradleRioVersion(currentYear === undefined ? TARGET_GRADLE_RIO_YEAR : currentYear, context) 36 | if (latestVer === undefined) { 37 | return 38 | } 39 | 40 | // Set local version to latest version if update is available 41 | if (localVer === latestVer) { 42 | return 43 | } 44 | await updateWorkspaceGradleRIOVersion(latestVer, workspaceDir) 45 | 46 | // Notify user that GradleRIO version has been updated 47 | vscode.window.showInformationMessage(`GradleRIO version in ${vscode.Uri.joinPath(workspaceDir.uri, "build.gradle").toString()} updated from ${localVer} to ${latestVer}`) 48 | }) 49 | } 50 | 51 | /** 52 | * updateWorkspaceGradleRIOVersion writes a new GradleRIO version to a target workspace's build.gradle file. 53 | * 54 | * @param newVersion GradleRIO version to write to the build.gradle 55 | * @param workspaceDir The workspace folder to find the target build.gradle in 56 | */ 57 | async function updateWorkspaceGradleRIOVersion(newVersion: string, workspaceDir: vscode.WorkspaceFolder) { 58 | const buildGradle = vscode.Uri.joinPath(workspaceDir.uri, "build.gradle") 59 | 60 | const currentData = await vscode.workspace.fs.readFile(buildGradle) 61 | const currentContents = Buffer.from(currentData).toString("utf8") 62 | 63 | const newContents = currentContents.replace(/id \"edu.wpi.first.GradleRIO\" version \".+\"/gi, `id "edu.wpi.first.GradleRIO" version "${newVersion}"`) 64 | 65 | await vscode.workspace.fs.writeFile(buildGradle, Buffer.from(newContents, "utf8")) 66 | } 67 | 68 | /** 69 | * getGradleRIOVersionFromWorkspace reads the build.gradle in the provided workspace, 70 | * looking for the currently defined GradleRIO version. 71 | * 72 | * @param workspaceDir The workspace folder to read the GradleRIO version from 73 | * @returns The current GradleRIO version, or null if it couldn't be found 74 | */ 75 | async function getGradleRIOVersionFromWorkspace(workspaceDir: vscode.WorkspaceFolder): Promise { 76 | let data: Uint8Array 77 | try { 78 | data = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle")) 79 | } catch (_) { 80 | return null 81 | } 82 | const contents = Buffer.from(data).toString("utf8") 83 | 84 | const currentVerArray = contents.match(/id\ "edu\.wpi\.first\.GradleRIO"\ version\ "(.*)"/) 85 | if (currentVerArray === null) { 86 | return "" 87 | } 88 | 89 | return currentVerArray[1] 90 | } 91 | 92 | /** 93 | * getLatestGradleRioVersion retrieves the latest GradleRIO version for the requested year. 94 | * 95 | * @param currentYear The year to get the latest version of GradleRIO for 96 | * @param context VSCode extension context 97 | * @returns The latest known version or undefined if the latest version for the requested year could not be found 98 | */ 99 | async function getLatestGradleRioVersion(currentYear: string, context: vscode.ExtensionContext): Promise { 100 | await updateGradleRIOVersionCache(context) 101 | 102 | const grvCacheValue: string | undefined = context.globalState.get("grvCache") 103 | 104 | if (grvCacheValue === undefined) { // Curse you Typescript! 105 | return undefined 106 | } 107 | 108 | return JSON.parse(grvCacheValue)[currentYear] 109 | } 110 | 111 | /** 112 | * updateGradleRIOVersionCache sends a request to plugins.gradle.org to get the latest version of GradleRIO 113 | * a maximum of once per hour, using VSCode global states to cache the data for use at a later time. 114 | * 115 | * @param context VSCode extension context, used to set global state values 116 | */ 117 | async function updateGradleRIOVersionCache(context: vscode.ExtensionContext) { 118 | const storedLastUpdate = context.globalState.get("lastGradleRioVersionUpdateTime", 0) as number; 119 | const currentTime = Date.now(); 120 | 121 | if (!(currentTime > storedLastUpdate + 86400000)) { 122 | return; 123 | } 124 | 125 | const latestVersions: { [key: string]: string } = await getLatestOnlineGradleRioVersions(); 126 | context.globalState.update("grvCache", JSON.stringify(latestVersions)); 127 | } 128 | 129 | /** 130 | * getLatestOnlineGradleRioVersions retrieves the latest version of GradleRIO for each year 131 | * that it has been published. 132 | * 133 | * @returns The latest version of GradleRIO for each year 134 | */ 135 | async function getLatestOnlineGradleRioVersions(): Promise<{ [key: string]: string }> { 136 | const responseString = await getGradleRIOVersionXML(); 137 | 138 | let resultObj = responseString.match(/(.*)<\/versions>/s); 139 | 140 | if (resultObj === null) { 141 | resultObj = ["", ""]; 142 | } 143 | 144 | const allVersionsString = resultObj[0]; 145 | 146 | const temp = allVersionsString.match(/([A-Za-z0-9.\-]*)<\/version>/g); 147 | 148 | let allVersions: string[] = []; 149 | 150 | temp?.forEach((element: string) => { 151 | allVersions.push(element.replace("", "").replace("", "")); 152 | }); 153 | 154 | let allVersionsLatest: { [key: string]: string } = {}; 155 | 156 | allVersions.forEach((element) => { 157 | let tempYear = element.match(/[0-9]{4}/); 158 | let year = "0000"; 159 | if (tempYear !== null) { 160 | year = tempYear[0]; 161 | } 162 | 163 | if (allVersionsLatest[year] !== undefined) { 164 | if (semver.satisfies(element, `> ${allVersionsLatest[year]}`)) { 165 | allVersionsLatest[year] = element; 166 | } 167 | } else { 168 | allVersionsLatest[year] = element; 169 | } 170 | }); 171 | 172 | return allVersionsLatest; 173 | } 174 | 175 | /** 176 | * getGradleRIOVersionXML gets the Maven metadata for the GradleRIO plugin on plugins.gradle.org. 177 | * 178 | * @returns An XML string, retrieved from plugins.gradle.org 179 | */ 180 | async function getGradleRIOVersionXML(): Promise { 181 | var response: any; 182 | try { 183 | response = await axios.get("https://plugins.gradle.org/m2/edu/wpi/first/GradleRIO/maven-metadata.xml"); 184 | } catch (error) { 185 | console.log(error); 186 | return ""; 187 | } 188 | return response.data as string; 189 | } 190 | -------------------------------------------------------------------------------- /src/template/providers.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { TemplateStrings } from "./templates" 3 | import { ITemplateProvider, TemplateType } from "./models" 4 | 5 | /** 6 | * TemplateProviderAggregator wraps the various template providers (integrated, user-level, and workspace-level) into 7 | * a single template provider that can be used by the templating system. 8 | * 9 | * This class prioritizes templates in the following order: workspace, user, integrated. 10 | */ 11 | export class TemplateProviderAggregator implements ITemplateProvider { 12 | integratedProvider: ITemplateProvider 13 | userProvider: ITemplateProvider 14 | workspaceProviders: Map 15 | 16 | constructor(integratedProvider: ITemplateProvider, userProvider: ITemplateProvider) { 17 | this.workspaceProviders = new Map() 18 | 19 | this.integratedProvider = integratedProvider 20 | this.userProvider = userProvider 21 | } 22 | 23 | /** 24 | * setWorkspaceProvider saves a template provider for a specific workspace URI 25 | * 26 | * @param uri The URI to save the template provider for 27 | * @param provider The provider to save 28 | */ 29 | setWorkspaceProvider(uri: vscode.Uri, provider: ITemplateProvider) { 30 | this.workspaceProviders.set(uri.toString(), provider) 31 | } 32 | 33 | /** 34 | * getWorkspaceProvider looks up the template provider associated with a given workspace URI. 35 | * 36 | * @param uri The URI to lookup 37 | * @returns The template provider for the provided URI 38 | */ 39 | getWorkspaceProvider(uri: vscode.Uri): ITemplateProvider | undefined { 40 | return this.workspaceProviders.get(uri.toString()) 41 | } 42 | 43 | /** 44 | * deleteWorkspaceProvider removes the template provider that matches the provided URI from 45 | * the aggregator's internal map. 46 | * 47 | * @param uri The URI to clear the template provider from 48 | */ 49 | deleteWorkspaceProvider(uri: vscode.Uri) { 50 | this.workspaceProviders.delete(uri.toString()) 51 | } 52 | 53 | async getTemplate(t: TemplateType, workspaceFolder: vscode.Uri): Promise { 54 | const workspaceProvider = this.getWorkspaceProvider(workspaceFolder) 55 | if (workspaceProvider === undefined) { 56 | const userResult = await this.userProvider.getTemplate(t, workspaceFolder) 57 | if (userResult !== null) { 58 | return userResult 59 | } 60 | return await this.integratedProvider.getTemplate(t, workspaceFolder) 61 | } 62 | 63 | const workspaceResult = await workspaceProvider.getTemplate(t, workspaceFolder) 64 | if (workspaceResult !== null) { 65 | return workspaceResult 66 | } 67 | 68 | const userResult = await this.userProvider.getTemplate(t, workspaceFolder) 69 | if (userResult !== null) { 70 | return userResult 71 | } 72 | 73 | return await this.integratedProvider.getTemplate(t, workspaceFolder) 74 | } 75 | } 76 | 77 | /** 78 | * FileSystemTemplateProvider is a general template provider that reads .kfftemplate files 79 | * directly from the file system. 80 | */ 81 | export class FileSystemTemplateProvider implements ITemplateProvider { 82 | topLevelUri: vscode.Uri 83 | 84 | constructor(topLevelUri: vscode.Uri) { 85 | this.topLevelUri = topLevelUri 86 | } 87 | 88 | async getTemplate(t: TemplateType, _: vscode.Uri): Promise { 89 | let readData: Uint8Array 90 | try { 91 | readData = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(this.topLevelUri, `${templateTypeToString(t)}.kfftemplate`)) 92 | } catch (_) { 93 | return null 94 | } 95 | 96 | const templateString = Buffer.from(readData).toString("utf8") 97 | 98 | return templateString 99 | } 100 | } 101 | 102 | /** 103 | * IntegratedTemplateProvider is the template provider that provides the default templates for 104 | * use in the templating system. The template strings are stored in the TemplateStrings class 105 | * which is auto-generated before compiling and bundling the Typescript. 106 | */ 107 | export class IntegratedTemplateProvider implements ITemplateProvider { 108 | templates: TemplateStrings 109 | 110 | constructor() { 111 | this.templates = new TemplateStrings() 112 | } 113 | 114 | async getTemplate(t: TemplateType, _: vscode.Uri): Promise { 115 | const tStr = this.templates[templateTypeToString(t) as keyof TemplateStrings] // Cast result from templateTypeToString to a key of TemplateStrings. StackOverflow: https://stackoverflow.com/a/62438434 116 | 117 | return tStr 118 | } 119 | } 120 | 121 | /** 122 | * templateTypeToString turns the TemplateType enum into a string which can used to acquire 123 | * templates from their storage locations. 124 | * 125 | * @param t The template type to lookup 126 | * @returns A string representing the provided template type 127 | */ 128 | function templateTypeToString(t: TemplateType): string { 129 | switch (t) { 130 | // Command based 131 | // General 132 | case TemplateType.commandRobot: 133 | return "commandRobot" 134 | case TemplateType.robotContainer: 135 | return "robotContainer" 136 | case TemplateType.commandConstants: 137 | return "commandConstants" 138 | // Commands 139 | case TemplateType.command: 140 | return "command" 141 | case TemplateType.instantCommand: 142 | return "instantCommand" 143 | case TemplateType.parallelCommandGroup: 144 | return "parallelCommandGroup" 145 | case TemplateType.parallelDeadlineGroup: 146 | return "parallelDeadlineGroup" 147 | case TemplateType.parallelRaceGroup: 148 | return "parallelRaceGroup" 149 | case TemplateType.PIDCommand: 150 | return "PIDCommand" 151 | case TemplateType.profiledPIDCommand: 152 | return "profiledPIDCommand" 153 | case TemplateType.sequentialCommandGroup: 154 | return "sequentialCommandGroup" 155 | case TemplateType.trapezoidProfileCommand: 156 | return "trapezoidProfileCommand" 157 | case TemplateType.commandExampleCommand: 158 | return "commandExampleCommand" 159 | case TemplateType.commandAutos: 160 | return "commandAutos" 161 | // Subsystems 162 | case TemplateType.subsystem: 163 | return "subsystem" 164 | case TemplateType.exampleSubsystem: 165 | return "exampleSubsystem" 166 | case TemplateType.PIDSubsystem: 167 | return "PIDSubsystem" 168 | case TemplateType.profiledPIDSubsystem: 169 | return "profiledPIDSubsystem" 170 | case TemplateType.trapezoidProfileSubsystem: 171 | return "trapezoidProfileSubsystem" 172 | // Skeleton 173 | case TemplateType.commandSkeletonRobot: 174 | return "commandSkeletonRobot" 175 | case TemplateType.commandSkeletonRobotContainer: 176 | return "commandSkeletonRobotContainer" 177 | 178 | // Romi 179 | case TemplateType.romiBuildGradle: 180 | return "romiBuildGradle" 181 | case TemplateType.romiTimedRobot: 182 | return "romiTimedRobot" 183 | case TemplateType.romiTimedDrivetrain: 184 | return "romiTimedDrivetrain" 185 | case TemplateType.romiCommandRobotContainer: 186 | return "romiCommandRobotContainer" 187 | case TemplateType.romiCommandConstants: 188 | return "romiCommandConstants" 189 | case TemplateType.romiCommandExampleCommand: 190 | return "romiCommandExampleCommand" 191 | case TemplateType.romiCommandDrivetrainSubsystem: 192 | return "romiCommandDrivetrainSubsystem" 193 | 194 | // Robot.kt files 195 | case TemplateType.robotBaseRobot: 196 | return "robotBaseRobot" 197 | case TemplateType.timedRobot: 198 | return "timedRobot" 199 | case TemplateType.timedSkeleton: 200 | return "timedSkeletonRobot" 201 | 202 | // Misc 203 | case TemplateType.main: 204 | return "main" 205 | case TemplateType.buildGradle: 206 | return "buildGradle" 207 | case TemplateType.emptyClass: 208 | return "emptyClass" 209 | 210 | default: 211 | throw new Error("Invalid TemplateType passed to templateTypeToString") 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kotlin for FRC Changelog 2 | 3 | ## [2024.1.1](https://github.com/BrenekH/kotlin-for-frc/releases/2024.1.1) 4 | 5 | **Enhancements:** 6 | 7 | - Update templates for 2024 8 | - Add Command Based Skeleton template and remove Robot Base template 9 | - Report projects as Kotlin instead of Java ([#151](https://github.com/BrenekH/kotlin-for-frc/issues/151)) 10 | 11 | ## [2023.5.1](https://github.com/BrenekH/kotlin-for-frc/releases/2023.5.1) 12 | 13 | **Fixed Bugs:** 14 | 15 | - Fix simulate command on Windows ([#145](https://github.com/BrenekH/kotlin-for-frc/issues/145) 16 | 17 | ## [2023.1.2](https://github.com/BrenekH/kotlin-for-frc/releases/2023.1.2) 18 | 19 | **Implemented enhancements:** 20 | 21 | - Fix deployment issues caused by SVGs in the README 22 | 23 | ## [2023.1.1](https://github.com/BrenekH/kotlin-for-frc/releases/2023.1.1) 24 | 25 | **Implemented enhancements:** 26 | 27 | - Update templates for 2023 season 28 | 29 | - Change target GradleRIO version and year to 2023 30 | 31 | ## [2022.10.1](https://github.com/BrenekH/kotlin-for-frc/releases/2022.10.1) 32 | 33 | **Implemented enhancements:** 34 | 35 | - Automatically publish builds to [open-vsx.org](https://open-vsx.org/extension/Brenek/kotlin-for-frc) 36 | 37 | - Put `wpilibsuite.vscode-wpilib` and `Brenek.kotlin-for-frc` in `.vscode/extensions.json` as recommended extensions 38 | 39 | - Updated logo to match the new Kotlin logo 40 | 41 | - Switch to `@vscode/test-electron` testing package (thanks [@anuragh2002](https://github.com/anuragh2002)) 42 | 43 | - New shorter extension description 44 | 45 | ## [2022.7.1](https://github.com/BrenekH/kotlin-for-frc/releases/2022.7.1) (2022-07-19) 46 | 47 | **Implemented enhancements:** 48 | 49 | - Updated upstream dependencies 50 | 51 | - Updated base GradleRIO version to 2022.4.1 52 | 53 | - Added support for Node 18 54 | 55 | ## [2022.2.1](https://github.com/BrenekH/kotlin-for-frc/releases/2022.2.1) (2022-02-01) 56 | 57 | **Implemented enhancements:** 58 | 59 | - Updated upstream dependencies 60 | 61 | ## [2022.1.2](https://github.com/BrenekH/kotlin-for-frc/releases/2022.1.2) (2022-01-16) 62 | 63 | **Implemented enhancements:** 64 | 65 | - Updated package versions to fix a security issue. 66 | 67 | - Updated linter action to newest major version. 68 | 69 | ## [2022.1.1](https://github.com/BrenekH/kotlin-for-frc/releases/2022.1.1) (2022-01-04) 70 | 71 | **Implemented enhancements:** 72 | 73 | - Removed Old Command-based templates 74 | 75 | - Updated templates to be compatible with 2022 WPILib 76 | 77 | ## [2021.8.1](https://github.com/BrenekH/kotlin-for-frc/releases/2021.8.1) (2021-08-30) 78 | 79 | **Implemented enhancements:** 80 | 81 | - Rewrote codebase to be more maintainable 82 | 83 | - Use VS Code tasks for code simulation instead of a terminal. 84 | 85 | - Better support for multiple workspace files 86 | 87 | - Better support for virtual file systems 88 | 89 | ## [2021.5.1](https://github.com/BrenekH/kotlin-for-frc/releases/2021.5.1) (2021-05-09) 90 | 91 | **Implemented enhancements:** 92 | 93 | - Limited functionality in Untrusted Workspaces 94 | 95 | - Disable KfF in Virtual Workspaces 96 | 97 | - Upgrade `lodash` to mitigate possible security issue 98 | 99 | ## [2021.4.1](https://github.com/BrenekH/kotlin-for-frc/releases/2021.4.1) (2021-04-24) 100 | 101 | **Implemented enhancements:** 102 | 103 | - Just some various dependency upgrades to mitigate potential security issues 104 | 105 | ## [2021.2.1](https://github.com/BrenekH/kotlin-for-frc/releases/2021.2.1) (2021-02-16) 106 | 107 | **Implemented enhancements:** 108 | 109 | - Upgraded internal GradleRIO version to 2021.2.2 110 | 111 | - Added the Scheduled Tags bot to GitHub repository for an enhanced deployment workflow [\#67](https://github.com/BrenekH/kotlin-for-frc/issues/67) 112 | 113 | ## [2021.1.2](https://github.com/BrenekH/kotlin-for-frc/releases/2021.1.2) (2021-01-11) 114 | 115 | **Implemented enhancements:** 116 | 117 | - Much smaller .vsix by removing README GIFs that link to the online version anyway 118 | 119 | ## [2021.1.1](https://github.com/BrenekH/kotlin-for-frc/releases/2021.1.1) (2021-01-06) 120 | 121 | **Implemented enhancements:** 122 | 123 | - Updated templates for the 2021 FRC season [\#60](https://github.com/BrenekH/kotlin-for-frc/issues/60) 124 | 125 | - GradleRIO version is now pulled from plugins.gradle.org, ensuring robot code is up-to-date without a new extension release [\#41](https://github.com/BrenekH/kotlin-for-frc/issues/41) 126 | 127 | - Kotlin for FRC settings are now handled through the native VSCode API instead of a custom JSON file \(`.kotlin-for-frc/kotlin-frc-preferences.json`\) [\#55](https://github.com/BrenekH/kotlin-for-frc/issues/55) 128 | 129 | - Added new command: `Kotlin-FRC: Simulate FRC Kotlin Code` [\#63](https://github.com/BrenekH/kotlin-for-frc/issues/63) 130 | 131 | - Templates are now configurable [\#30](https://github.com/BrenekH/kotlin-for-frc/issues/30) 132 | 133 | **Fixed bugs:** 134 | 135 | - PIDCommand needs a wrapper in order to operate properly [\#35](https://github.com/BrenekH/kotlin-for-frc/issues/35) 136 | 137 | ## [2020.2.1](https://github.com/BrenekH/kotlin-for-frc/releases/2020.2.1) (2020-02-22) 138 | 139 | **Implemented enhancements:** 140 | 141 | - Update to GradleRIO 2020.3.2 142 | 143 | ## [2020.1.1](https://github.com/BrenekH/kotlin-for-frc/releases/2020.1.1) (2020-01-06) 144 | 145 | **Implemented enhancements:** 146 | 147 | - New Command API [\#19](https://github.com/BrenekH/kotlin-for-frc/issues/19) 148 | - Use github-changelog-generator for CHANGELOG.md [\#16](https://github.com/BrenekH/kotlin-for-frc/issues/16) 149 | 150 | **Fixed bugs:** 151 | 152 | - .kotlin-for-frc appears even when it's not needed [\#24](https://github.com/BrenekH/kotlin-for-frc/issues/24) 153 | 154 | **Merged pull requests:** 155 | 156 | - Development into release-2020.1.1 [\#32](https://github.com/BrenekH/kotlin-for-frc/pull/32) ([BrenekH](https://github.com/BrenekH)) 157 | - 2020 to development [\#31](https://github.com/BrenekH/kotlin-for-frc/pull/31) ([BrenekH](https://github.com/BrenekH)) 158 | 159 | ## [1.5.0](https://github.com/BrenekH/kotlin-for-frc/releases/1.5.0) (2019-11-20) 160 | 161 | **Implemented enhancements:** 162 | 163 | - Update extension to use vscode.workspace.fs [\#17](https://github.com/BrenekH/kotlin-for-frc/issues/17) 164 | - Remove Main.java to Main.kt [\#14](https://github.com/BrenekH/kotlin-for-frc/issues/14) 165 | 166 | **Merged pull requests:** 167 | 168 | - Release 1.5.0 into master [\#25](https://github.com/BrenekH/kotlin-for-frc/pull/25) ([BrenekH](https://github.com/BrenekH)) 169 | - Create CONTRIBUTING.md [\#21](https://github.com/BrenekH/kotlin-for-frc/pull/21) ([BrenekH](https://github.com/BrenekH)) 170 | - 2019 changelog merge into development branch [\#20](https://github.com/BrenekH/kotlin-for-frc/pull/20) ([BrenekH](https://github.com/BrenekH)) 171 | 172 | ## 1.4.0 - 2019-06-30 173 | 174 | - Faster extension loading times using webpack 175 | 176 | - Actually started using the GradleRIO version updater method 177 | 178 | - Increased the amount of the codebase covered by tests 179 | 180 | ## 1.3.0 - 2019-03-21 181 | 182 | - When updating the GradleRIO version, the GradleRIO version is the only line that changes 183 | 184 | - Added tests to ensure quality code 185 | 186 | ## 1.2.1 - 2019-01-22 187 | 188 | - Changed gradle main class back to frc.robot.Main 189 | 190 | - Added JvmStatic annotation to Main.kt 191 | 192 | - Travis CI GitHub releases 193 | 194 | ## 1.2.0 - 2019-01-21 195 | 196 | - Fixed Main.java not finding Robot class while linting 197 | 198 | - This means that Main.java is replaced by Main.kt 199 | 200 | - Updating of GradleRio Version 201 | 202 | - Under the hood abstraction 203 | 204 | - New commands related to checking the integrity of the build.gradle and Main.kt files 205 | 206 | - Travis CI integration 207 | 208 | ## 1.1.1 - 2019-01-08 209 | 210 | - Fixed dependency issue that caused create new class/command here to stop working 211 | 212 | - Fixed package auto-detection 213 | 214 | ## 1.1.0 - 2019-01-07 215 | 216 | - Changed Main.kt to Main.java to comply with static requirement of WPILib 217 | 218 | - Changed main folder from kotlin to java to allow Main.java to interface with Gradle properly 219 | 220 | ## 1.0.0 - 2019-01-04 221 | 222 | - FULL RELEASE! 223 | 224 | - Converted all templates to match WPILib templates for the 2019 FRC Season 225 | 226 | - Implemented auto detection of package when creating new classes 227 | 228 | - New scheme for creating new classes 229 | 230 | - Added TODO for easier collection of ideas for extension development 231 | 232 | ## 0.4.0 - 2018-12-18 233 | 234 | - Addressed Issue #2 and added support for Iterative, Sample, and Timed Robot types 235 | 236 | ## 0.3.1 - 2018-11-26 237 | 238 | - Fixed security hole by removing flatmap-stream dependency 239 | 240 | ## 0.3.0 - 2018-11-26 241 | 242 | - Added dialog box to clear confusion about when the conversion from Java to Kotlin was complete 243 | 244 | ## 0.2.1 - 2018-11-09 245 | 246 | - Fixed bug where the extension would immediately crash 247 | 248 | ## 0.2.0 - 2018-11-08 249 | 250 | - Added Icon 251 | 252 | ## 0.1.1 - 2018-11-07 253 | 254 | - Housekeeping changes 255 | 256 | ## 0.1.0 - 2018-11-07 257 | 258 | - Initial release 259 | -------------------------------------------------------------------------------- /.github/actions/post-release-checklist/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./index.js" /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */, 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | "removeComments": true /* Disable emitting comments. */, 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 74 | 75 | /* Type Checking */ 76 | "strict": true /* Enable all strict type-checking options. */, 77 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 78 | "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, 79 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, 80 | "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, 81 | "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, 82 | "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */, 83 | "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */, 84 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 85 | "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, 86 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, 87 | "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, 88 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, 89 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, 90 | "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, 91 | "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, 92 | "noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type */, 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/commands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { SIMULATE_CODE_TASK_NAME, TARGET_GRADLE_RIO_VER } from "../constants" 3 | import { executeCommand } from "../tasks/cmdExecution" 4 | import { ITemplateProvider } from "../template/models" 5 | import { showChangelog } from "../util/changelog" 6 | import updateGradleRioVersion from "../util/gradleRioUpdate" 7 | import { getJavaHomeGradleArg, } from "../util/util" 8 | import { writeCommandBased, writeRomiCommandBased, writeRomiTimed, writeTimed, writeTimedSkeleton, writeCommandBasedSkeleton } from "./conversion" 9 | import { RobotType } from "./models" 10 | import { createFileWithContent, determineRobotType, parseTemplate } from "./util" 11 | import { TemplateType } from "../template/models" 12 | import { ensureExtensionsRecommended } from "../util/recommendations" 13 | 14 | /** 15 | * ICommandExecutor defines the behavior the simulateFRCKotlinCode function requires 16 | * from an object which aims to run the simulation command. 17 | */ 18 | interface ICommandExecutor { 19 | /** 20 | * execute executes a given command. 21 | * 22 | * @param cmd The command to run 23 | * @param name The name of tab to use 24 | * @param workspaceFolder The workspace folder to execute the command in 25 | */ 26 | execute(cmd: string, name: string, workspaceFolder: vscode.WorkspaceFolder): void 27 | } 28 | 29 | /** 30 | * registerCommands simply registers all of the required command handlers with the VSCode 31 | * Extension Host. 32 | * 33 | * @param context VSCode extension context, used to add command handlers 34 | * @param templateProvider The ITemplateProvider to use for any commands which deal with templates 35 | */ 36 | export async function registerCommands(context: vscode.ExtensionContext, templateProvider: ITemplateProvider) { 37 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.convertJavaProject", async () => { 38 | if (vscode.workspace.workspaceFolders === undefined) { 39 | vscode.window.showErrorMessage("Kotlin-FRC: Cannot convert project without an open workspace.") 40 | return 41 | } 42 | 43 | let workspaceDir = vscode.workspace.workspaceFolders[0] 44 | if (vscode.workspace.workspaceFolders.length > 1) { 45 | const temp = await vscode.window.showWorkspaceFolderPick() 46 | if (temp === undefined) { 47 | return 48 | } 49 | workspaceDir = temp 50 | } 51 | 52 | const robotJavaUri = vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "java", "frc", "robot", "Robot.java") 53 | let robotJava: string 54 | try { 55 | const robotJavaData = await vscode.workspace.fs.readFile(robotJavaUri) 56 | robotJava = Buffer.from(robotJavaData).toString("utf8") 57 | } catch (e) { 58 | console.error(e) 59 | vscode.window.showErrorMessage(`Kotlin-FRC: Could not read ${robotJavaUri.toString()}. Cancelling project conversion.`) 60 | return 61 | } 62 | 63 | const buildGradleUri = vscode.Uri.joinPath(workspaceDir.uri, "build.gradle") 64 | let buildGradle: string 65 | try { 66 | const buildGradleData = await vscode.workspace.fs.readFile(buildGradleUri) 67 | buildGradle = Buffer.from(buildGradleData).toString("utf8") 68 | } catch (e) { 69 | console.error(e) 70 | vscode.window.showErrorMessage(`Kotlin-FRC: Could not read ${buildGradleUri.toString()}. Cancelling project conversion.`) 71 | return 72 | } 73 | 74 | const projectRobotType = determineRobotType(robotJava, buildGradle) 75 | 76 | // Delete existing files 77 | const toDelete = vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "java") 78 | await vscode.workspace.fs.delete(toDelete, { recursive: true }) 79 | 80 | // Add new files with parsed template contents 81 | switch (projectRobotType) { 82 | case RobotType.command: 83 | writeCommandBased(workspaceDir, templateProvider) 84 | break 85 | case RobotType.commandSkeleton: 86 | writeCommandBasedSkeleton(workspaceDir, templateProvider) 87 | break 88 | case RobotType.romiCommand: 89 | writeRomiCommandBased(workspaceDir, templateProvider) 90 | break 91 | case RobotType.romiTimed: 92 | writeRomiTimed(workspaceDir, templateProvider) 93 | break 94 | case RobotType.timed: 95 | writeTimed(workspaceDir, templateProvider) 96 | break 97 | case RobotType.timedSkeleton: 98 | writeTimedSkeleton(workspaceDir, templateProvider) 99 | break 100 | 101 | default: 102 | vscode.window.showErrorMessage("Kotlin-FRC: Unknown RobotType for conversion. Cancelling...") 103 | return 104 | } 105 | 106 | ensureExtensionsRecommended(workspaceDir, ["Brenek.kotlin-for-frc", "wpilibsuite.vscode-wpilib"]) 107 | 108 | vscode.window.showInformationMessage("Kotlin-FRC: Conversion complete!") 109 | })) 110 | 111 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.createNew", async (filePath: vscode.Uri) => { 112 | vscode.window.showQuickPick(["Command-Based", "Empty Class"]).then((result: string | undefined) => { 113 | switch (result) { 114 | case "Command-Based": 115 | vscode.window.showQuickPick(["Command", "Subsystem"]).then((result: string | undefined) => { 116 | switch (result) { 117 | case "Command": 118 | vscode.window.showQuickPick([ 119 | TemplateType.command, 120 | TemplateType.instantCommand, 121 | TemplateType.parallelCommandGroup, 122 | TemplateType.parallelDeadlineGroup, 123 | TemplateType.parallelRaceGroup, 124 | TemplateType.PIDCommand, 125 | TemplateType.profiledPIDCommand, 126 | TemplateType.sequentialCommandGroup, 127 | TemplateType.trapezoidProfileCommand, 128 | ]).then((result: string | undefined) => { 129 | if (result === undefined) { return } 130 | 131 | createNewFromTemplate(result as TemplateType, templateProvider, filePath) 132 | }) 133 | break 134 | case "Subsystem": 135 | vscode.window.showQuickPick([ 136 | TemplateType.subsystem, 137 | TemplateType.PIDSubsystem, 138 | TemplateType.profiledPIDSubsystem, 139 | TemplateType.trapezoidProfileSubsystem, 140 | ]).then((result: string | undefined) => { 141 | if (result === undefined) { return } 142 | 143 | createNewFromTemplate(result as TemplateType, templateProvider, filePath) 144 | }) 145 | break 146 | default: 147 | return 148 | } 149 | }) 150 | break 151 | case "Empty Class": 152 | createNewFromTemplate(TemplateType.emptyClass, templateProvider, filePath) 153 | break 154 | default: 155 | return 156 | } 157 | }) 158 | })) 159 | 160 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.showChangelog", async () => { 161 | showChangelog() 162 | })) 163 | 164 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.resetAutoShowChangelog", async () => { 165 | context.globalState.update("lastInitVersion", "0.0.0") 166 | })) 167 | 168 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.updateGradleRIOVersion", async () => { 169 | updateGradleRioVersion(true, context) 170 | })) 171 | 172 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.resetGradleRIOCache", async () => { 173 | context.globalState.update("grvCache", "") 174 | context.globalState.update("lastGradleRioVersionUpdateTime", 0) 175 | })) 176 | 177 | context.subscriptions.push(vscode.commands.registerCommand("kotlinForFRC.simulateFRCKotlinCode", simulateFRCKotlinCode({ 178 | execute: (cmd: string, name: string, workspaceFolder: vscode.WorkspaceFolder) => { 179 | executeCommand(cmd, name, workspaceFolder) 180 | } 181 | }))) 182 | } 183 | 184 | /** 185 | * simulateFRCKotlinCode builds and returns a function that can be used as a callback for vscode.commands.registerCommand. 186 | * This should not be used outside of its original file. It is only exported for testing purposes. 187 | * 188 | * @returns Function that is callable by vscode as a command 189 | */ 190 | export function simulateFRCKotlinCode(cmdExecutor: ICommandExecutor): (...args: any[]) => any { 191 | return async () => { 192 | if (!vscode.workspace.isTrusted) { 193 | vscode.window.showErrorMessage("Kotlin-FRC: Cannot simulate code while the workspace is untrusted.") 194 | return 195 | } 196 | 197 | if (vscode.workspace.workspaceFolders === undefined) { 198 | vscode.window.showErrorMessage("Kotlin-FRC: Cannot simulate code without an open workspace.") 199 | return 200 | } 201 | 202 | let workspaceDir = vscode.workspace.workspaceFolders[0] 203 | if (vscode.workspace.workspaceFolders.length > 1) { 204 | const temp = await vscode.window.showWorkspaceFolderPick() 205 | if (temp === undefined) { 206 | return 207 | } 208 | workspaceDir = temp 209 | } 210 | 211 | cmdExecutor.execute(`./gradlew simulateJava ${getJavaHomeGradleArg()}`, SIMULATE_CODE_TASK_NAME, workspaceDir) 212 | } 213 | } 214 | 215 | /** 216 | * createNewFromTemplate creates a new file with the provided information, while also prompting 217 | * the user for a name. 218 | * 219 | * @param templateType The template to parse 220 | * @param templateProvider The provider to pull the template from 221 | * @param dirPath The directory to put the new file in 222 | */ 223 | async function createNewFromTemplate(templateType: TemplateType, templateProvider: ITemplateProvider, dirPath: vscode.Uri): Promise { 224 | const workspaceDir = vscode.workspace.getWorkspaceFolder(dirPath) 225 | if (workspaceDir === undefined) { return } 226 | 227 | const templateContents = await templateProvider.getTemplate(templateType, workspaceDir.uri) 228 | if (templateContents === null) { return } 229 | 230 | const className = await vscode.window.showInputBox({ placeHolder: `Name your ${templateType.toString()}` }) 231 | if (className === undefined) { return } 232 | 233 | createFileWithContent(vscode.Uri.joinPath(dirPath, `${className}.kt`), parseTemplate(templateContents, className, determinePackage(dirPath), TARGET_GRADLE_RIO_VER)) 234 | } 235 | 236 | /** 237 | * 238 | * @param filePath The path to form into a package directory 239 | * @returns 240 | */ 241 | export function determinePackage(filePath: vscode.Uri): string { 242 | const workspaceDir = vscode.workspace.getWorkspaceFolder(filePath) 243 | if (workspaceDir === undefined) { return "frc.robot" } 244 | 245 | const mainFolderUri = vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin") 246 | 247 | // mainFolderUri.path + "/" is required because just the path leaves behind a leading / 248 | return filePath.path.replace(mainFolderUri.path + "/", "").replace(/\//g, ".") 249 | } 250 | -------------------------------------------------------------------------------- /src/commands/conversion.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { TARGET_GRADLE_RIO_VER } from "../constants"; 3 | import { ITemplateProvider, TemplateType } from "../template/models"; 4 | import { createFileWithContent, parseTemplate } from "./util"; 5 | 6 | /** 7 | * writeCommandTemplate creates the necessary directories and files for a Command-based Kotlin project. 8 | * 9 | * @param workspaceDir The workspace folder to create the new project in 10 | * @param templateProvider The provider to pull templates from 11 | */ 12 | export async function writeCommandBased(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 13 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "commands")) 14 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "subsystems")) 15 | 16 | const buildGradle = await templateProvider.getTemplate(TemplateType.buildGradle, workspaceDir.uri) as string 17 | nullTemplateCheck(buildGradle) 18 | 19 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 20 | nullTemplateCheck(main) 21 | 22 | const robot = await templateProvider.getTemplate(TemplateType.commandRobot, workspaceDir.uri) as string 23 | nullTemplateCheck(robot) 24 | 25 | const constants = await templateProvider.getTemplate(TemplateType.commandConstants, workspaceDir.uri) as string 26 | nullTemplateCheck(constants) 27 | 28 | const robotContainer = await templateProvider.getTemplate(TemplateType.robotContainer, workspaceDir.uri) as string 29 | nullTemplateCheck(robotContainer) 30 | 31 | const exampleSubsystem = await templateProvider.getTemplate(TemplateType.exampleSubsystem, workspaceDir.uri) as string 32 | nullTemplateCheck(exampleSubsystem) 33 | 34 | const exampleCmd = await templateProvider.getTemplate(TemplateType.commandExampleCommand, workspaceDir.uri) as string 35 | nullTemplateCheck(exampleCmd) 36 | 37 | const cmdAutos = await templateProvider.getTemplate(TemplateType.commandAutos, workspaceDir.uri) as string 38 | nullTemplateCheck(cmdAutos) 39 | 40 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 41 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 42 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 43 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Constants.kt"), parseTemplate(constants, "Constants", "frc.robot", TARGET_GRADLE_RIO_VER)) 44 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "RobotContainer.kt"), parseTemplate(robotContainer, "RobotContainer", "frc.robot", TARGET_GRADLE_RIO_VER)) 45 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "subsystems", "ExampleSubsystem.kt"), parseTemplate(exampleSubsystem, "ExampleSubsystem", "frc.robot.subsystems", TARGET_GRADLE_RIO_VER)) 46 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "commands", "ExampleCommand.kt"), parseTemplate(exampleCmd, "ExampleCommand", "frc.robot.commands", TARGET_GRADLE_RIO_VER)) 47 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "commands", "Autos.kt"), parseTemplate(cmdAutos, "Autos", "frc.robot.commands", TARGET_GRADLE_RIO_VER)) 48 | } 49 | 50 | /** 51 | * writeCommandBasedSkeleton creates the necessary directories and files for a Command-based skeleton Kotlin project. 52 | * 53 | * @param workspaceDir The workspace folder to create the new project in 54 | * @param templateProvider The provider to pull templates from 55 | */ 56 | export async function writeCommandBasedSkeleton(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 57 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot")) 58 | 59 | const buildGradle = await templateProvider.getTemplate(TemplateType.buildGradle, workspaceDir.uri) as string 60 | nullTemplateCheck(buildGradle) 61 | 62 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 63 | nullTemplateCheck(main) 64 | 65 | const robot = await templateProvider.getTemplate(TemplateType.commandSkeletonRobot, workspaceDir.uri) as string 66 | nullTemplateCheck(robot) 67 | 68 | const robotContainer = await templateProvider.getTemplate(TemplateType.commandSkeletonRobotContainer, workspaceDir.uri) as string 69 | nullTemplateCheck(robotContainer) 70 | 71 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 72 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 73 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 74 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "RobotContainer.kt"), parseTemplate(robotContainer, "RobotContainer", "frc.robot", TARGET_GRADLE_RIO_VER)) 75 | } 76 | 77 | /** 78 | * writeRomiCommand creates the necessary directories and files for a Romi Command-based Kotlin project. 79 | * 80 | * @param workspaceDir The workspace folder to create the new project in 81 | * @param templateProvider The provider to pull templates from 82 | */ 83 | export async function writeRomiCommandBased(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 84 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "commands")) 85 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "subsystems")) 86 | 87 | const buildGradle = await templateProvider.getTemplate(TemplateType.romiBuildGradle, workspaceDir.uri) as string 88 | nullTemplateCheck(buildGradle) 89 | 90 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 91 | nullTemplateCheck(main) 92 | 93 | // The Romi command template uses the exact same Robot class file as normal command-based, so just use that. 94 | const robot = await templateProvider.getTemplate(TemplateType.commandRobot, workspaceDir.uri) as string 95 | nullTemplateCheck(robot) 96 | 97 | const constants = await templateProvider.getTemplate(TemplateType.romiCommandConstants, workspaceDir.uri) as string 98 | nullTemplateCheck(constants) 99 | 100 | const robotContainer = await templateProvider.getTemplate(TemplateType.romiCommandRobotContainer, workspaceDir.uri) as string 101 | nullTemplateCheck(robotContainer) 102 | 103 | const subsystem = await templateProvider.getTemplate(TemplateType.romiCommandDrivetrainSubsystem, workspaceDir.uri) as string 104 | nullTemplateCheck(subsystem) 105 | 106 | const exampleCmd = await templateProvider.getTemplate(TemplateType.romiCommandExampleCommand, workspaceDir.uri) as string 107 | nullTemplateCheck(exampleCmd) 108 | 109 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 110 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 111 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 112 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Constants.kt"), parseTemplate(constants, "Constants", "frc.robot", TARGET_GRADLE_RIO_VER)) 113 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "RobotContainer.kt"), parseTemplate(robotContainer, "RobotContainer", "frc.robot", TARGET_GRADLE_RIO_VER)) 114 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "subsystems", "RomiDrivetrain.kt"), parseTemplate(subsystem, "RomiDrivetrain", "frc.robot.subsystems", TARGET_GRADLE_RIO_VER)) 115 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "commands", "ExampleCommand.kt"), parseTemplate(exampleCmd, "ExampleCommand", "frc.robot.commands", TARGET_GRADLE_RIO_VER)) 116 | } 117 | 118 | /** 119 | * writeRomiTimed creates the necessary directories and files for a Romi TimedRobot Kotlin project. 120 | * 121 | * @param workspaceDir The workspace folder to create the new project in 122 | * @param templateProvider The provider to pull templates from 123 | */ 124 | export async function writeRomiTimed(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 125 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot")) 126 | 127 | const buildGradle = await templateProvider.getTemplate(TemplateType.romiBuildGradle, workspaceDir.uri) as string 128 | nullTemplateCheck(buildGradle) 129 | 130 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 131 | nullTemplateCheck(main) 132 | 133 | const robot = await templateProvider.getTemplate(TemplateType.romiTimedRobot, workspaceDir.uri) as string 134 | nullTemplateCheck(robot) 135 | 136 | const romiDrivetrain = await templateProvider.getTemplate(TemplateType.romiTimedDrivetrain, workspaceDir.uri) as string 137 | nullTemplateCheck(romiDrivetrain) 138 | 139 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 140 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 141 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 142 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "RomiDrivetrain.kt"), parseTemplate(romiDrivetrain, "RomiDrivetrain", "frc.robot", TARGET_GRADLE_RIO_VER)) 143 | } 144 | 145 | /** 146 | * writeTimed creates the necessary directories and files for a TimedRobot Kotlin project. 147 | * 148 | * @param workspaceDir The workspace folder to create the new project in 149 | * @param templateProvider The provider to pull templates from 150 | */ 151 | export async function writeTimed(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 152 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot")) 153 | 154 | const buildGradle = await templateProvider.getTemplate(TemplateType.buildGradle, workspaceDir.uri) as string 155 | nullTemplateCheck(buildGradle) 156 | 157 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 158 | nullTemplateCheck(main) 159 | 160 | const robot = await templateProvider.getTemplate(TemplateType.timedRobot, workspaceDir.uri) as string 161 | nullTemplateCheck(robot) 162 | 163 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 164 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 165 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 166 | } 167 | 168 | /** 169 | * writeTimedSkeleton creates the necessary directories and files for a Skeleton TimedRobot Kotlin project. 170 | * 171 | * @param workspaceDir The workspace folder to create the new project in 172 | * @param templateProvider The provider to pull templates from 173 | */ 174 | export async function writeTimedSkeleton(workspaceDir: vscode.WorkspaceFolder, templateProvider: ITemplateProvider) { 175 | await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot")) 176 | 177 | const buildGradle = await templateProvider.getTemplate(TemplateType.buildGradle, workspaceDir.uri) as string 178 | nullTemplateCheck(buildGradle) 179 | 180 | const main = await templateProvider.getTemplate(TemplateType.main, workspaceDir.uri) as string 181 | nullTemplateCheck(main) 182 | 183 | const robot = await templateProvider.getTemplate(TemplateType.timedSkeleton, workspaceDir.uri) as string 184 | nullTemplateCheck(robot) 185 | 186 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "build.gradle"), parseTemplate(buildGradle, "", "", TARGET_GRADLE_RIO_VER)) 187 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Main.kt"), parseTemplate(main, "Main", "frc.robot", TARGET_GRADLE_RIO_VER)) 188 | createFileWithContent(vscode.Uri.joinPath(workspaceDir.uri, "src", "main", "kotlin", "frc", "robot", "Robot.kt"), parseTemplate(robot, "Robot", "frc.robot", TARGET_GRADLE_RIO_VER)) 189 | } 190 | 191 | /** 192 | * nullTemplateCheck displays an error message to the user and throws an error 193 | * if it is passed a null value. 194 | * 195 | * This is meant to be used as a to ensure all templates are actual strings before 196 | * trying to create files with them. 197 | * 198 | * @param target Value to check for null 199 | */ 200 | function nullTemplateCheck(target: string | null) { 201 | if (target === null) { 202 | vscode.window.showErrorMessage("Kotlin-FRC: Received a null template. Cancelling...") 203 | throw new Error("Got null template") 204 | } 205 | } 206 | --------------------------------------------------------------------------------