Easier Editing for .replit Files

Colton Donnelly

At Replit, we want to make it easy to support any project configuration possible. This includes ensuring your binaries are in the right place and intelligent code completion works. Historically, the .replit file has been at the center of this, but the experience of editing the file is a process shrouded in mystery. While documentation exists for the file and what various configurations imply, it’s much more convenient if the documentation is visible inside the Workspace.

Starting today, you’ll be able to edit your .replit file fearlessly. We’re now providing intelligent code completion and documentation for all .replit files, powered by Taplo - an LSP server for TOML files. Happy configuring!

How we did it

With Taplo, the only requirement for providing TOML linting and LSP functionality is a JSON Schema for the TOML file being described. Since we consume the .replit file in a Go codebase, we could use the Go jsonschema module to use our already-existing field tags for TOML deserialization in our JSON Schema properties. This means we were able to generate a JSON Schema without changing our struct definition!

type DotReplit struct {
      Run Command `toml:"run"`
      Compile Command `toml:"compile"`
      Languages map[string]LanguageConfig `toml:"languages"`
      // ...
}

Unfortunately, the JSON schema we generated at this step was incomplete; the Command type is a little weird because it can be a string, an array of strings, or an object. Internally, only the object format is used, and we have custom deserialization logic to turn the string and array of strings cases into the Object type:

// We use this Command type as a proxy to the SysCommand
// type so that we can customize deserialization logic
// for the string and array of strings cases, while
// reusing the Go runtime reflection deserialization for
// the standard SysCommand type.
type Command struct {
      SysCommand
}

type SysCommand struct {
      args []string `toml:"args"`
      env map[string]string `toml:"env,omitempty"`
}

Since Command was already a proxy object, and we want to document how the Command type is used broadly, we developed a custom schema generator function that is called instead of the reflection-based generator:

func (c Command) JSONSchema() *jsonschema.Schema {
	r := &jsonschema.Reflector{
		FieldNameTag: "toml",
	}

	tableSchema := r.Reflect(SysCommand{})

	return &jsonschema.Schema{
		AnyOf: []*jsonschema.Schema{
			tableSchema.Definitions["SysCommand"],
			{
				Type: "string",
			},
			{
				Type: "array",
				Items: &jsonschema.Schema{
					Type: "string",
				},
			},
		},
	}
}

LSP is also helpful for providing documentation, which we don’t achieve with this. While the jsonschema module mentioned above does support creating schema descriptions from Go comments in the source file, we have a few too many internal-facing comments in the file to reasonably include in this public-facing documentation format. So, we used another feature in jsonschema to describe the documentation for some fields:

type DotReplit struct {
      Run Command `toml:"run" jsonschema_description:"The command to run when the Run button is pressed."`
      Audio bool `toml:"audio" 
jsonschema_description:"Whether to enable audio support in the Repl through pulseaudio."`
    // ...
}

However, this description wasn’t showing up in the LSP-provided documentation, and since the Command type is used so often in the .replit file, we needed another hack to get proper documentation working:

type DotReplit struct {
	Run RunCommand `toml:"run"`
	// ...
}

type RunCommand struct {
	Command
}

func (c RunCommand) JSONSchema() *jsonschema.Schema {
	commandSchema := Command{}.JSONSchema()
	commandSchema.Description = strings.Join(
		[]string{
			"The command to run when the Repl boots.",
			"",
			commandSchemaDef.Description,
		},
		"\n",
	)

	return commandSchema
}

This implementation generated a JSON schema from our Go type definition, complete with descriptions! And, we could extend the Command description to apply to each of the different contexts in which we accept a Command. Now that the LSP server was correctly configured for the .replit file, we just needed a few more steps to get it working for all Repls.

Some of you may have heard about our novel Nix modules system. Nix modules allow us to deliver Repl configurations using the power of Nix to ensure that these configurations are correct and up-to-date. While we’ve been using Nix Modules to provide language- and framework-specific configurations (like Rust and SvelteKit), recently, we added a new module simply identified as “replit.” This module has been made special internally to always be loaded, and as such, available in every Repl. This module only currently adds the LSP configuration for the .replit file. We may find more configurations crucial to making Replit a seamless experience - keep an eye out for future changes to this module!

We hope you’ll enjoy the improved experience the next time you edit your .replit file! If you found this interesting and want to help solve problems like this, apply to join the Developer Experience team at Replit!

More blog posts