Vue: input data validation with zod

This post consists from two parts - the first one is a small introduction to the zod library, and the second one is about how we can use it for form validation in Vue.

Source code

Zod

Zod is a javascript library for data validation. It has pretty small bundle size, works with typescript and javascript, and it’s easy to start with.

Philosophy

Zod is a library for data validation - it doesn’t work with UI elements - only with plain js data. It means that you can use zod on a server side, as well as on a client. It also means that you can’t use it for form validation until form’s input controls are not bounded to a javascript data structure(s).

Object validation

To validate something with zod, you first need to create a validation schema - a special object that will parse the data.

import {z} from "zod";

const userSchema = z.object({
	name: z.string(),
	age: z.number()
	});

userSchema.parse({name: "Username"});

All fields are required by default. To make some property optional, use optional():

import {z} from "zod";

const userSchema = z.object({
	name: z.string(),
	age: z.number().optional() // number | undefined
	});

userSchema.parse({name: "Username"});

Parse method returns a deep copy of input variable or throws an error if a value doesn’t pass validation. As an alternative, we can use safeParse method, which returns either a copy of input variable or an error. A little remark about return types - in zod, there’s a Input type and a Output type. Usually, they’re the same. But since zod supports data transformation, the output type may differ from the input type.

Schema reusability

We can re-use already defined schemas:

import {z} from "zod";

const contactsSchema = z.object({
	email: z.string().email(),
	phone: z.string().max(50)
	});

const userSchema = z.object({
	name: z.string().max(50),
	contacts: contactsSchema
	});

Here, we’ve reused contactsSchema inside userSchema declaration. In general, it’s a good idea to decompose complex schemas into smaller ones - it’s the same thing as creating functions to handle complex logic.

Refinements

Refinements in zod allow you to create your own validation logic. Documentation is pretty clear on how to use refinements, so I won’t copy examples here.

Using zod for input validation in Vue

Form

We will validate a form with personal user information. It will contain these fields:

  • Username (required, max 50 characters)
  • Email (required)
  • Real name (optional, max 100 characters)
  • City (required, max 100 characters)
  • Work email (by default has the same value as Email)

In this form, the field with work email will be hidden by default, and a “Same as the main email” checkbox will hide/show it. If it’s visible, it’s required. If it’s not visible, it’s not required, because it has the same value as the “Email” field.

First, we need to create a new Vue project:

npm create vue@3

We don’t need any libraries except typescript, so don’t forget to include it in the project during creation.

So, here our starting point - a form with input fields, where each input is bounded to a data model, and a “Submit” button. When a user clicks the button, state of the form is showing up. There’re no any error checks yet, but we’ll add them later.

<script setup lang="ts">
<script setup lang="ts">
import {shallowReactive} from "vue";

interface PersonalInfo {
  username?: string;
  email?: string;
  name?: string;
  city?: string;
  workEmail?: string;

  /**
   * True if workEmail is the same as main email
   */
  sameEmail: boolean;
}


const formData = shallowReactive<FormData>({
  sameEmail: true
})

function onSubmit() {
  alert(JSON.stringify(formData, null, 2));
}

</script>

<template>
<div class="container">
  <h1>Personal info</h1>
    <div class="input">
      <label for="username"> Username </label>
      <input name="username" v-model="formData.username"/>
    </div>
    <div class="input" >
      <label for="email"> Email </label>
      <input name="email" v-model="formData.email"/>
    </div>
    <div class="input">
      <label for="name"> Real name </label>
      <input name="name" v-model="formData.name"/>
    </div>
    <div class="input">
      <label for="city"> City  </label>
      <input name="city" v-model="formData.city"/>
    </div>
    <div>
      <label for="sameEmail"> Work email is the same as the main  </label>
      <input type="checkbox" name="sameEmail" v-model="formData.sameEmail" />
    </div>
    <div v-if="!formData.sameEmail" class="input">
      <label for="workemail"> Work email  </label>
      <input name="workemail" v-model="formData.workEmail"/>
    </div>

    <button @click="onSubmit"> Submit </button>
  </div>
</template>

<style scoped>
.input {
display: flex;
  gap: 0.3rem;
  flex-direction: column;
}
.container {
  display: flex;
  gap: 1rem;
  flex-direction: column;
}

button {
  font-size: 1.5rem;
  margin-top: 1.5rem;
  background-color: blue;
}
</style>

</script>

<template>
<div class="container">
  <h1>Personal info</h1>
    <div class="input">
      <label for="username"> Username </label>
      <input name="username"/>
    </div>
    <div class="input" >
      <label for="email"> Email </label>
      <input name="email" />
    </div>
    <div class="input">
      <label for="name"> Real name </label>
      <input name="name" />
    </div>
    <div class="input">
      <label for="city"> City  </label>
      <input name="city" />
    </div>
    <div class="input">
      <label for="workemail"> Work email  </label>
      <input name="workemail" />
    </div>

    <button> Submit </button>
  </div>
</template>

<style scoped>
.input {
display: flex;
  gap: 0.3rem;
  flex-direction: column;
}
.container {
  display: flex;
  gap: 1rem;
  flex-direction: column;
}

button {
  font-size: 1.5rem;
  margin-top: 1.5rem;
  background-color: blue;
}
</style>

Now our form looks like this:

Form screenshot

And when a user clicks the “Submit” button, we show the value of the formData variable:

Form screenshot

Display error messages

Our form now accepts all possible string values, without any restrictions. It’s time to add validation and tell the user what fields were filled are incorrect. Let’s add error messages to the input fields (you can check out input css class in the full source code at the end of the post):

<div class="input">
  <label for="city"> City  </label>
  <input name="city" v-model="formData.city"/>
  <div class="error"> city error </div>
</div>

Now each of our fields has an error label:

Form screen

What we want now is to validate all fields and show corresponding error messages for all invalid fields when a user clicks the “Submit” button. First of all, we need a validation schema:

const personalSchema = z.object({
  username: z.string().max(50),
  email: z.string().email(),
  name: z.string().max(100),
  city: z.string().max(100),
  sameEmail: z.boolean(),
  workEmail: z.string().optional()
}).refine((val) => {
  const emailRegex =
  /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;

  return val.sameEmail || emailRegex.test(val.workEmail)
}, {message: "Invalid email", path: ["workEmail"]})

It doesn’t look very simple for such task, though. Let’s take a closer look at what we have here.

username: z.string().max(50),
email: z.string().email(),
name: z.string().max(100),
city: z.string().max(100),
sameEmail: z.boolean(),

These lines describe rules for our fields, and everything is straightforward here.

...
workEmail: z.string().optional()
}).refine((val) => {
  const emailRegex =
  /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;

  return val.sameEmail || (val.workEmail ? emailRegex.test(val.workEmail) : false)
}, {message: "Invalid email", path: ["workEmail"]})

This is the root of our schema’s ugliness. The workEmail field is optional when the sameEmail flag is true, and when this flag is false, workEmail should be validated as an email field. In zod, for validations that require a context (and we need one here, because the result of validation relies on another field’s value), the refine() method is used. Its first argument is a function that accepts the whole schema as a parameter, and the truthiness of its result leads to passing the validation. The second parameter is settings - we set the error message and and the error’s path.

This line returns true if the sameFlag is true, or returns the result of email validation, which is done by regular expression. I’ve grabbed this regex from zod’s source code, by the way:

return val.sameEmail || (val.workEmail ? emailRegex.test(val.workEmail) : false)

Run validation

First of all, we need zod itself:

npm i zod

We want to show error messages only when a user presses the “Submit” button. After that, any changes in input fields should re-launch validation. Also, we need a convenient way to get error messages. First, we need a variable that will hold either the submit button was pressed or not:

const isValidationPerformed = shallowRef(false)

Then, we create a computed variable that returns the list of validation errors:

const errors = computed(() => {
    if (!isValidationPerformed.value) {
      return undefined;
    }

    const validationResult = personalSchema.safeParse(formData)
    return validationResult.success ? undefined : validationResult.error.format()

})

A few more notes:

  • We use safeParse because we don’t want to throw errors

  • We use error.format() to format our errors into a convenient form. This method returns an object with messages in this kind of format:

    {
    	username: {
    		_errors: ["Required field"]
    	},
    	email: {
    		_errors: ["Invalid email"]
    	}
    	city: {
    		_errors: ["Required field"]
    	},
    
    }
    

Inside onSubmit function we set isValidationPerformed to true:

isValidationPerformed.value = true;

And finally, we need to display our error messages. We always display only first error message from the list:

<div class="input">
    <label for="username"> Username </label>
    <input name="username" v-model="formData.username"/>
    <div class="error"> {{errors?.username?._errors[0]}} </div>
</div>

We use optional chaining, because we don’t know if the specified field exists in the errors object.

How it works

  1. By default, isValidationPerformed is set to false. User can edit any field, and no error messages will be shown.
  2. When a user clicks the “Submit” button, we set isValidationPerformed variable to true.
  3. isValidationPerformed is a reactive dependency in the errors computed, so it’s recalculated when isValidationPerformed has changed.
  4. If errors has any errors, they’re shown under the input fields.
  5. Since isValidationPerformed is true, any changes in the formData object also trigger errors computed recalculation, so when a user fixes an input value, the corresponding error message is disappeared (again, because of error’s recalculation)

Dealing with empty strings

If we press the “Submit” button when the form is empty, the “Required” error will be shown under our fields:

Form screenshot

When we type something in, for example, the “username” field, the error under the input field is disappeared:

Form screenshot

But if we clear the field, no any error message will be shown:

Form screenshot

That’s because now our formData.username contains an empty string, not an undefined value, and this situation fully satisfies the validation condition (it’s a string and its length don’t overflow the limit). If we want to forbid empty and space-only strings, we may use trim() and min() combination:

// Create a separate zod schema for non-empty strings
const nonEmptyString = z.string().trim().min(1);

const personalSchema = z.object({
  username: nonEmptyString.max(50),
  email: nonEmptyString.email(),
  name: nonEmptyString.max(100),
  city: nonEmptyString.max(100),
  sameEmail: z.boolean(),
  workEmail: nonEmptyString.optional()
}).
refine((val) => {
  const emailRegex =
  /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;

  return val.sameEmail || emailRegex.test(val.workEmail)
}, {message: "Invalid email", path: ["workEmail"]})

Now everything works fine:

Form validation screenshot

The full code

<script setup lang="ts">
import {shallowReactive, shallowRef, computed} from "vue";
import {z} from "zod";

export interface PersonalInfo {
  username?: string;
  email?: string;
  name?: string;
  city?: string;
  workEmail?: string;

  /**
   * True if workEmail is the same as main email
   */
  sameEmail: boolean;
}

const nonEmptyString = z.string().trim().min(1);

const personalSchema = z.object({
  username: nonEmptyString.max(50),
  email: nonEmptyString.email(),
  name: nonEmptyString.max(100),
  city: nonEmptyString.max(100),
  sameEmail: z.boolean(),
  workEmail: nonEmptyString.optional()
}).
refine((val) => {
  const emailRegex =
  /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;

  return val.sameEmail || (val.workEmail ? emailRegex.test(val.workEmail) : false)
}, {message: "Invalid email", path: ["workEmail"]})

const isValidationPerformed = shallowRef(false)


const formData = shallowReactive<PersonalInfo>({
  sameEmail: true
})


const errors = computed(() => {
    if (!isValidationPerformed.value) {
      return undefined;
    }

    const validationResult = personalSchema.safeParse(formData)

    return validationResult.success ? undefined : validationResult.error.format()

})


function onSubmit() {
  isValidationPerformed.value = true;

  if (!errors.value) {
    alert(JSON.stringify(formData, null, 2));
    alert("All good!")
  } else {
    alert(JSON.stringify(errors.value, null, 2))
  }
}

</script>

<template>
<div class="container">
  <h1>Personal info</h1>
    <div class="input">
      <label for="username"> Username </label>
      <input name="username" v-model="formData.username"/>
      <div class="error"> {{errors?.username?._errors[0]}} </div>
    </div>
    <div class="input" >
      <label for="email"> Email </label>
      <input name="email" v-model="formData.email"/>
      <div class="error">  {{errors?.email?._errors[0]}} </div>
    </div>
    <div class="input">
      <label for="name"> Real name </label>
      <input name="name" v-model="formData.name"/>
      <div class="error">  {{errors?.name?._errors[0]}} </div>
    </div>
    <div class="input">
      <label for="city"> City  </label>
      <input name="city" v-model="formData.city"/>
      <div class="error">  {{errors?.city?._errors[0]}} </div>
    </div>
    <div>
      <label for="sameEmail"> Work email is the same as the main  </label>
      <input type="checkbox" name="sameEmail" v-model="formData.sameEmail" />
    </div>
    <div v-if="!formData.sameEmail" class="input">
      <label for="workemail"> Work email  </label>
      <input name="workemail" v-model="formData.workEmail"/>
      <div class="error">  {{errors?.workEmail?._errors[0]}} </div>
    </div>

    <button @click="onSubmit"> Submit </button>
  </div>
</template>

<style scoped>
.input {
display: flex;
  gap: 0.3rem;
  flex-direction: column;
}
.container {
  display: flex;
  gap: 1rem;
  flex-direction: column;
}

button {
  font-size: 1.5rem;
  margin-top: 1.5rem;
  background-color: blue;
}

.error {
  margin-top: -0.5rem;
  color: red;
  font-size: 0.5rem;
}
</style>

Additional information and similar libraries

Schema type inference

In our example, we declared our form interface first, but it’s also possible to infer the whole type from a zod schema:

type PersonalInfo = z.infer<typeof personalSchema>;

Similar libraries

There’re also:

  • Vee validate. Probably the most popular validation solution for Vue. I personally don’t like it much, yet it doesn’t cancel the fact that this is a great library.
  • Vuelidate. Vue framework that validates data. Generally, it has the same concept as in our implementation.
  • Yup. For me, one of the benefits of yup over zod is the when() method, with which you can create conditional validation. By the way, vee-validate uses yup by default as a validation engine.


.env validation with zod

Summary: You can use zod library to validate a .env file before building a project. Zod’s capabilities allow to handle very complex cases, though they are very rare.

Have you ever been in a situation when you built your frontend project and found that some value in your .env file was incorrect, or even missed? I’ve been.

After the last time when this happened to me, I decided to add validation to the build step.

Add validation to a project

  1. Install zoddotenv and tsx packages as dev dependencies
    npm i -D zod dotenv tsx
    
  2. Create a validate-env.ts file in a scripts directory:
     import 'dotenv/config'
     import process from 'node:process'
     import { z } from 'zod'
    
     const envSchema = z.object({
         VITE_BASE_API: z.string().url()
     })
    
     envSchema.parse(process.env)
    
  3. Add this to a build script in the package.json:
    build: "tsx scripts/validate-env.ts && vite build"
    

In my example I used vite and validated a single variable. You can add as many rules as you need.

Testing

Delete the .env file if it exists, and run npm run build. Zod will throw an error, because the VITE_BASE_API variable is not valid:

ZodError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "undefined",
    "path": [
      "VITE_BASE_API"
    ],
    "message": "Required"
  }
]

Add the VITE_BASE_API variable to your .env, but leave it empty:

VITE_BASE_API=''

Now zod throws another error, because provided value is not a valid url:

ZodError: [
  {
    "validation": "url",
    "code": "invalid_string",
    "message": "Invalid url",
    "path": [
      "VITE_BASE_API"
    ]
  }
]

Keep in mind

When you add such thing to your project, you take additional responsibility for updating validation rules.

I Hope it was useful to someone, good luck.

Links

My Workflowy review

In this post I’ll tell about my experience with Workflowy - famous outliner service, which I’ve been using for half a year at the moment of writing.

Why

I don’t suffer from information gathering much, but I have pretty minimal needs - sync bookmarks, keep some text notes and todo items. I used to keep bookmarks with Firefox sync mechanism and my text notes/todo items with the help of protectedtext (which is a great service!). But because firefox became unusable, I switched a browser from Firefox to Vivaldi on a smartphone, while on a laptop I was still using firefox. That broke my bookmarks workflow, and at that moment I had two bookmark lists, and each of them was living its own life. What about notes? - I was happy with how I dealt with them, but it changed over time because of bad usability on a smartphone. I tried a lot of note management tools (Trello, TickTick, Notion, Org mode, Simplenote, Standardnotes, Google Keep), but finally I’ve picked Workflowy. The main reason was that I thought that I would be able to adopt it for all my needs (And strictly speaking, I succeed at this). I was using it for less than a month before I bought PRO subscription. Now the story begins…

Interface (Desktop)

Workflowy has a great UI. It’s clean and consistent, what else do I need? In general, nothing. There are some issues with the mobile app - I’ll list them in the next section.

Here, it’s worth mentioning DynaList - a clone of Workflowy, which in my opinion has a terrible user interface:

Interface (Mobile)

This is a list of my issues with the mobile interface:

  • I just can’t tap on those small arrows to expand a node
  • Slow and draggy animation

Everything else is very nice.

Keyboard shortcuts

I had been using only mouse for the first few months, but when I tried to use a keyboard, I was thrilled. Now, on desktop/web platform, about half of my actions are done with a keyboard. You can always toggle the help panel with shortcuts in three steps:

  1. Click options button
  2. Pick “Settings”
  3. Toggle the “help me learn the keyboard shortcuts”

My day to day shortcuts are:

  • Ctrl - arrow up/arrow down - Expand/collapse a node
  • Alt - arrow left/arrow right - Zoom out/zoom in
  • Ctrl - Alt - m - Move a node
  • Ctrl - Enter - Mark a node as completed
  • Ctrl - Shift - Backspace - delete a node
  • Shift - Enter - Add a new node

Slow startup

On a smartphone, the application starts very slow. I hate it when I take the smartphone to write out a quick note and have to wait more than 5 seconds for application to be loaded. Any default note taking application starts in a moment on any android device, and such slow Workflowy’s start speed is very irritating. I’m not the only one who have faced with this - there’s a ticket with this problem. The support says that 10-15 seconds for startup is “expecting” and “pretty standard for now”.

In the march of 2023, Workflowy announced “Significant speed improvements” of load time - now it’s 40% less in average. However, I personally haven’t noticed any speed improvements - maybe this is because I have a very small amount of list items, while they seem like they have boosted load time only for accounts with a large number of nodes, I don’t know.

Complex content

Workflowy is an outliner. It’s poorly suited for rich content with images, paragraphs, tables etc. With this service, you should consider every node as one single, atomic block of the whole text. Do you want to insert an image? Create a node. Need a list? Create a node for each item. Want to split some notes into paragraphs? You know what to do. And this way of working with documents may be unacceptable for people who want to use Workflowy for writing long text and complex documents.

Kanban

They’re selling it as the feature that would incline Trello users to make a switch to Workflowy.

I think this is silly. Trello sucks, but if someone uses it, Workflowy’s unintuitive kanban implementation will just enhance his attachment to Trello.

Summary

In summary, I like Workflowy, but I’m afraid that the team will be adding more and more features to it in pursuit of new users, and as a result, Workflowy will become everything, and nothing. Will I renew my subscription? Definitely not - I don’t use it heavily enough to pay for it.

Vue js: on keeping all components in a single directory

The point of article: keep components in one single directory with a flat structure.

Almost all Vue projects have a components directory, where all application’s components are kept. There are plenty of ways how you can organize them, but I think it’s better to put all your components right into components directory and do not create any sub-directories trying to make the structure obvious and simple. It may sounds strange, but in practice exactly this way is more simple and future-proof.

A flat components structure

As was told, the flat structure implies that components’ directory have no any sub-directories, just component files. Besides the components themselves, this directory contains their tests, if any. This way of organizing components has some benefits over non-flat structure:

  • It’s easier to see the whole picture

  • Imports are shorter:

    import Avatar from "./Avatar.vue"
    

    No ugly paths, like this:

    import Avatar from "@/components/main/header/Avatar.vue"
    import TextLabel from "../../Text/TextLabel.vue"
    
  • You won’t spend your time deciding where should you put your new component. This is a really good one. If you have a card with an article author’s avatar and a header with the same avatar, where should you put the “Avatar.vue”? In a “header” subdirectory or in a “card”? Or maybe create a “shared” directory and put all repetitive components here? It’s hard to decide actually. Moreover, often, when we create a new component, we don’t know whether it will be used somewhere else or not. With the flat structure, it’s not an issue.

  • Such structure forces you to re-use your components. If you group components into different directories based on their functionality, business logic or visual hierarchy, then you wouldn’t try to check whether a similar component already exists or not, or at least if you do, it will be hard to explore it.

  • Resistance to changes. no more need to move component between directories and globally rename it because now it’s not in the TodoList but in Shared directory.

Official Vue style guides

Applying official components’ naming convention dramatically increase understanding of what’s going on with your components. Consider this list of components:

Button.vue
MainLogoutButton.vue
LogoutButton.vue
Checkbox.vue
TextInput.vue
Sidebar.vue
Header.vue
AppButton.vue
AppTextInput.vue
AppCheckbox.vue
LogoutButton.vue
TheLogoutButton.vue
TheSidebar.vue
TheHeader.vue

At first glance it looks like there’s no differences and both lists provide the same information, but these styles of naming are very different. Let’s take MainLogoutButton.vue and LogoutButton.vue from the first example. What’s the difference between them? Most probably we need to check their sources, because just their names don’t tell us anything. But in the second example, we know that the TheLogoutButton.vue is the component that is instantiated only once per page, or even the whole application, and most probably it has some specific application logic that don’t allow to re-use this component, though it’s not a requirement.

The App prefix from the second example also tells us that those components don’t use any shared application state or implement any logic (such components are often called “dumb”) - they’re used only for visual representation.

Separate directories for pages and layouts

It’s very common to use pages (views) that represent whole pages of the application, and layouts directory with components that control high-order visual hierarchy ( for example, in a Profile layout we should always see TheHeader component at the top of the page, but in the Dashboard layout it shouldn’t be presented). Some frameworks (Nuxt) or plugins for vue (unplugin-vue-routervite-plugin-pages) expect those directories, so if you use them, it’s ok to move such kind of components outside the components directory, just keep track of these directories cleanliness - they shouldn’t contain non-page or non-layout components.

Links


Sublime text basics

Tabs

  • Ctrl - n - create new tab
  • Ctrl - w - close current tab
  • Ctrl - tab - move to the next tab

Command palette

If you don’t know hotkeys for a command, or don’t know if such a command even exists, use command palette. It’s the first place where you should search for unknown commands. To open the command palette, press Ctrl - Shift - p, and then type command name. For example, you want to transform selected text to upper case. This is what you should do:

  1. Ctrl - Shift - p
  2. Type text “lower”
  3. Select “Convert case: Lower case” command ( moving arrows ), and press Enter (or click )

Sidebar

  • Ctrl - 0 - focus on sidebar
  • Ctrl-k, Ctrl-b - toggle sidebar

Layouts

Sublime Text allows you to open a few tabs side by side.

  • Alt - Shift - 1 - leave only one edit buffer
  • Alt - Shift - 2 - split window vertically ( 2 tabs )
  • Alt - Shift - 3 - split window vertically by (3 tabs)
  • Alt - Shift - 4 - split window vertically by (4 tabs)
  • Alt - Shift - 5 - “Grid layout”. Two windows in a row
  • Alt - Shift - 8 - Split horizontally, 2 tabs
  • Alt - Shift - 9 - Split horizontally, 3 tabs

You can move between layout windows with Ctrl + <number> hotkeys. Ctrl - 0 - move to the sidebar, Ctrl - 1 - move to the first tab, Ctrl - 2 - move to the second tab, and so on.

Line manipulation

  • Ctrl - Shift - arrow up/arrow down - move the current line up/down
  • Ctrl - c - copy the whole line
  • Ctrl - x - cut the whole line
  • Ctrl - Shift - arrow up/down - move the line up/down

How to install packages (plugins)

First, you need to install package control. It’s the Sublime Text package manager.

  1. Open control palette (Ctrl - Shift - p)
  2. Type “Install package control”, press enter
  3. When package control is installed, open control palette and type “Install package” - press enter.
  4. Then start typing package name, for example “Color scheme”. Sublime Text will show related packages.

Multiple cursors

Sublime Text has a great support for working with multiple cursors. To set an additional cursor, use Ctrl - click. Then, all commands ( like moving cursor, char/word deletion etc) will be applied to all cursors.

There’s also nice feature in Sublime Text - you can select next occurance of the current word. It works this way:

  1. Press Ctrl - d on current word. It will be selected.
  2. Press Ctrl - d one more time. The next occurance of this word will be selected.

This is useful when you want, for example, delete or replace some occurances of a word.

Working with code

These commands may be useful for programmers.

  • Ctrl - m - move between brackets. Works even if the cursor is placed in the middle of text between them.
  • Ctrl - Shift - m - select text between brackets
  • Ctrl - g - go to line
  • Ctrl - ; - open dialog with all words in the current document. Start typing, then select required word with Enter. Then you can move to the next occurance of the word with F3

Maybe you don't need Latex?

If you don’t write a math book, you probably don’t need Latex. Just install LibreOffice, create a document in LibreOffice Writer, then click File - Export as - Export directly as PDF.

That’s all, and you don’t need to:

  1. Install Latex
  2. Install a TON of packages/fonts/language packs for it.
  3. Read answers on latex forums/stackoverflow (stackexcahange) for hours just to be able to generate a PDF file.
  4. Learn unintuitive markup language, time for learning which will never pay off.


How to install packages on startup in Emacs

Put these lines at the beginning of your .emacs:

; list the packages you want
(setq package-list '(
	web-mode
	projectile
	))

; list the repositories containing them
(setq package-archives '(("elpa" . "http://tromey.com/elpa/")
                         ("gnu" . "http://elpa.gnu.org/packages/")
                         ("marmalade" . "http://marmalade-repo.org/packages/")))

; activate all the packages (in particular autoloads)
(package-initialize)

; fetch the list of packages available
(unless package-archive-contents
  (package-refresh-contents))

; install the missing packages
(dolist (package package-list)
  (unless (package-installed-p package)
    (package-install package)))

This solution I’ve grabbed here

Atom text editor's sunset

"A hackable editor for the 21st Century”, they said.

How it was at the beginning

In 2014-2015 ( I’ve found snapshots of atom.io website for 2014 year on waybackmachine, but Wikipedia says that it was released in 2015), Github released Atom ( I personally can’t recall when I first heard of Atom) - “A hackable editor for the 21st Century”. It quickly became very popular, because… I don’t know why, I never liked it. Anyway, it’s the fact. Atom was a loud name. None of “Best editors for…” types of articles and blog posts hadn’t mentioned Atom as a honorable member of their lists. It was built using web technologies ( that wasn’t so popular as now ), it was free, and a language for its extension was Javascript - one of the most popular languages. Because of electron, Atom was RAM expensive for a text editor ( especially in comparison to Sublime Text, Emacs, Vim or Notepad++ ), but for most users it wasn’t an issue, because Atom was in trend and beside that it had some really nice features - for free and out of the box.

Microsoft bought Github

In 2018, Microsoft acquired Github. It was obvious that Microsoft won’t support Atom because it had its own product - VSCode, which is a mainstream editor/IDE now. I think that VSCode has drained a lot of ideas and features from Atom:

  • Technology (Electron)
  • Extension language
  • Built-in interface for working with git repositories
  • Collaborative editing (In Atom , it was called “Teletype”, in VSCode, it’s the “Live Share” feature)

Over the years, VSCode was actively evolving ( not always in a good direction ) and became more and more popular across developers. Let’s see at stackoverflow developer survey results.

  • 2022 - 1st place (74.48%); Atom - 12 place (9.35%)
  • 2021 - 1st place (71.06%); Atom - 10 place (12.94%)
  • 2020 - I have not found info about tools for this survey
  • 2019 - 1st place (50.7%); Atom - 10 place (13.3%)
  • 2018 - 1st place (34.9%); Atom - 9 place (18.0%)
  • 2017 - 5 place (24%); Atom - 7 place (20%)
  • 2016 - 13 place (7.2%); Atom - 9 place (12.5%)
  • 2016 - Not presented; Atom - 5 place (2.8%)

In 2019, the next year after acquiring Github, VSCode made a huge jump - from 34 to 50%.

By the way, Wunderlist service ( a todo app) was shut down the same way - it was bought by Microsoft and later closed in favor of Microsoft todo.

Sunset

In the summer of 2022, Github published a blog post, where they announced that they’re sunsetting Atom.

Atom editor sunset

Pulsar

Community has forked Atom and reincarnated it as Pulsar editor.

Official website

It’s mostly an Atom editor (that’s good), just with a new name. According to the Goals page, Pulsar’s main goal is to keep Atom and its huge package base alive and up to date. It’s a great idea.

Sublime text plugins that work

Essentially my Sublime text setup requires only three things:

  • Terminus - terminal emulator right in the editor. The nice thing is that you can open terminal in a special panel, or you can open a terminal in a new tab.
  • Sublime LSP - LSP support
  • WordingStatus - Shows words count and statistics for a document.


Far manager

Far manager is a terminal-based file manager for Windows.

Far manager screen

Links

Basic commands

  • Tab - jump between left and right panels
  • F5 - Copy file under cursor/selection to the opposite panel
  • F6 - Move file under cursor/selection to the opposite panel
  • Shift-F5 - Create copy of a file file in the same panel
  • Shift-F6 - Rename current file
  • Enter - open file
  • Shift+arrows up/down - select files
  • F3 - View file under cursor
  • F4 - Edit file under cursor

Good parts

  • Good out of the box support for a large variety of archives
  • Free

Bad parts

  • Works only on Windows, because uses Win API
  • Current Major version of Far Manager is 3. Version 2 was far more lightweight
  • Now it hasn’t an option for turning on “Black and white” colorscheme.
  • Ugly default “Blue” color that makes you sick

More articles