Plain text and code
Table of Contents
This chapter describes how Kalem reads and writes files that are not Org,
Markdown, CSV or LaTeX: plain text, source code, configuration files and
every other text file. It also describes the file layer all modes share
(encodings, byte order marks, line endings, saving) and what happens to
files that are not text. The code is crates/kalem-core/src/mode.rs
(which mode a file gets), files.rs (reading and writing),
document.rs (indentation, pasting), code.rs (brackets, comments,
Enter) and the highlighter, crates/kalem-highlight.
1. The oracle
For a plain text file the oracle is the file itself: opened and saved without an edit, it stays byte for byte; opened, edited and saved, it differs only in the bytes of the edit. Kalem has no model of a text file beyond its characters, so there is nothing else to agree with.
What is tested: the byte order mark and CR LF surviving a round trip,
line feeds inserted by commands getting their carriage return in a CR
LF file, UTF-16 LE and BE with a byte order mark round-tripping exactly,
Windows-1254 text guessed from its bytes and written back to the same
bytes, characters an encoding cannot hold, reopening in a chosen
encoding (the decoding and encodings tests of files.rs); atomic
saving, backups, change detection, symbolic links and permissions
(saving, links_and_permissions); and an Org file with CR LF edited,
saved and reloaded (files in document.rs). There is no corpus test
of arbitrary files yet.
2. Which mode a file gets
A document has one mode (DocumentMode): Org, Markdown, CSV, LaTeX,
text with an optional language, binary (not opened) or a folder (the
file manager). When a file is opened, the mode is decided in this order;
the first rule that applies wins.
- Binary. Unless the file starts with a UTF-16 byte order mark, its
first 8,192 bytes (after a UTF-8 byte order mark) are checked (
looks_binary): a NUL byte, or bytes that are not UTF-8 (not merely a character cut at the end of the sample) with control characters making up more than a twentieth of the sample, make it binary. Text in a legacy encoding such as Windows-1254 has no control characters and is not taken for binary. A binary file is not opened (see Files that are not text). - The user's choice. Set Document Mode (
view.setMode) records the mode for the file in the workspace settings, in the tablefiles.modesof the nearest.kalem/settings.tomlabove the file (created beside it when there is none), keyed by the file's path relative to that workspace with/between names. The value isorg,markdown,csv,latexortext, or the language chosen for a text file (python). When the file is opened again, in either editor, this mode replaces the detected one. - A mode line. On the first line (after a byte order mark), or on
the second when the first is a
#!line (as Emacs reads it), the text between two-*-markers: eithermode: NAMEamong;-separatedkey: valuepairs, or a bareNAMEwhen there is no colon. The name is lower-cased. With a trailing-moderemoved, a name of one of the modes below selects that mode (org-mode,markdown,csv,latex…); any other name is taken as the language of a text file, as written (so-*- python -*-highlights Python, while-*- mode: python-mode -*-names the unknown languagepython-mode). - A
Local Variables:block. In the last 3,000 characters of the file, a line holdingLocal Variables:starts a block that ends at a line holdingEnd:; the text beforeLocal Variables:on its line is a prefix, and the text after it a suffix, removed from each line of the block (so# mode: organd/* mode: org */both work). Amode: NAMEline there names the mode as a mode line does. As in Emacs'sset-auto-mode, it comes before the file name. The extension. The file name's last extension, lower-cased:
Extensions Mode org,org_archiveOrg md,markdown,mdown,mkd,gfmMarkdown csv,tsv,tabCSV tex,latex,ltxLaTeX any other Text, the extension as its language Any extension decides, so a script named
run.shis shell by its extension whatever its first line says, andnotes.txtis text with the languagetxt. One exception: a.txtfile whose first lines (up to twenty non-empty ones, at least two) all hold the same number of tabs, at least one, with a value before the first tab in most of them, is CSV: this is Excel's "Unicode Text" export (UTF-16 with a byte order mark, tab-separated).- A shebang. Only for a file without an extension: a first line
starting with
#!. The program is its first word, or the word after it when the first word ends in/env; its last path component with trailing digits and dots removed is the language (#!/usr/bin/env python3givespython,#!/bin/shgivessh). - Plain text, without a language.
A file that does not exist yet (opened by name to be created) gets its mode from its name alone, UTF-8 without a byte order mark, and LF line endings (CR LF on Windows).
The names Set Document Mode accepts are org, markdown, csv,
text or plain, the names and extensions of the table above (md,
tex, latex…), and any other word, which is taken as a language:
view.setMode with python gives text highlighted as Python. Open as
Plain Text in a CSV file switches to text for the session only.
In when-clauses the mode is editorMode (org, markdown, csv,
latex, text, binary, directory); the language of a text file is
the command scope's text type, lower-cased (rs, python, bib).
3. Highlighting
A text file with a language is coloured by the highlighter
(kalem_highlight), which runs Sublime Text syntax definitions through
syntect with pure Rust regular expressions (fancy-regex). The language name is
resolved (Language::find) in this order, ignoring letter case:
- Org's source-block language names that differ from the syntaxes' names (the alias table below);
- a syntax whose name or extensions match the name (
syntect's token search); - a syntax that claims the name as an extension.
The syntax "Plain Text" is never used, so txt files are not coloured.
A name that resolves to nothing leaves the file uncoloured, with every
editing feature of text files still working.
The aliases:
| Names | Syntax |
|---|---|
sh shell bash zsh |
Bourne Again Shell (bash) |
elisp emacs-lisp lisp scheme |
Lisp |
clojure |
Clojure |
js javascript |
JavaScript |
ts typescript |
TypeScript |
tsx |
TypeScriptReact |
h |
C |
toml |
TOML |
ini conf |
INI |
python py |
Python |
rust |
Rust |
c |
C |
cpp c++ |
C++ |
latex tex |
LaTeX |
sql sqlite |
SQL |
R |
R |
haskell ocaml java go ruby perl php html css xml yaml json markdown makefile diff lua matlab scala erlang |
The syntax of that name |
dot |
Graphviz (DOT) |
The syntaxes and the extensions they claim: the default set of
syntect 5.3 with the syntaxes of bat 0.26.1 added, through the
two-face crate (0.5.2). An entry that is a whole file name
(Makefile, Cargo.lock) is listed as the syntax claims it, but the
language is looked up by extension only (see below). The table is made
by cargo run -p kalem-highlight --example syntax_table.
| Syntax | Extensions |
|---|---|
| ActionScript | as |
| Ada | adb ads gpr |
| Apache Conf | envvars htaccess HTACCESS htgroups HTGROUPS htpasswd HTPASSWD .htaccess .HTACCESS .htgroups .HTGROUPS .htpasswd .HTPASSWD |
| AppleScript | applescript script editor |
| AsciiDoc (Asciidoctor) | adoc ad asciidoc |
| ASP | asa |
| Authorized Keys | authorized_keys pub authorized_keys2 |
| AWK | awk |
| Batch File | bat cmd |
| BibTeX | bib |
| Bourne Again Shell (bash) | sh bash zsh ash .bash_aliases .bash_completions .bash_functions .bash_login .bash_logout .bash_profile .bash_variables .bashrc .profile .textmate_init .zlogin .zlogout .zprofile .zshenv .zshrc PKGBUILD ebuild eclass |
| C | c h |
| C# | cs csx |
| C++ | cpp cc cp cxx c++ C h hh hpp hxx h++ inl ipp |
| Cabal | cabal |
| camlp4 | |
| Cargo Build Results | |
| CFML | cfml cfm cfc |
| CFML Script | |
| CFML Script (Tags) | |
| Clojure | clj cljc cljs edn |
| CMake | CMakeLists.txt cmake |
| CMake C Header | h.in |
| CMake C++ Header | hh.in hpp.in hxx.in h++.in |
| CMakeCache | CMakeCache.txt |
| CMakeCommands | |
| CoffeeScript | coffee Cakefile coffee.erb cson |
| Comma Separated Values | |
| Command Help | cmd-help help |
| commands-builtin-shell-bash | |
| CpuInfo | cpuinfo |
| Crontab | tab crontab cron.d |
| Crystal | cr |
| CSS | css css.erb css.liquid |
| D | d di |
| Dart | dart |
| debsources | sources.list |
| Diff | diff patch |
| DMD Output | |
| Dockerfile | Dockerfile dockerfile .Dockerfile |
| Dockerfile (with bash) | |
| DotENV | .env .env.dist .env.local .env.sample .env.example .env.template .env.test .env.test.local .env.testing .env.dev .env.development .env.development.local .env.prod .env.production .env.production.local .env.dusk.local .env.staging .env.default .env.defaults .envrc .flaskenv env env.example env.sample env.template |
| Elixir | ex exs |
| Elm | elm |
| Elm Compile Messages | |
| Elm Documentation | |
eml msg mbx mboxz |
|
| Erlang | erl hrl Emakefile emakefile escript |
| F# | fs fsi fsx |
| Fish | fish |
| Fortran (Fixed Form) | f F f77 F77 for FOR fpp FPP |
| Fortran (Modern) | f90 F90 f95 F95 f03 F03 f08 F08 |
| Fortran Namelist | namelist |
| fstab | fstab crypttab mtab |
| GDScript (Godot Engine) | gd |
| GFortran Build Results | |
| Git Attributes | attributes gitattributes .gitattributes |
| Git Commit | COMMIT_EDITMSG MERGE_MSG TAG_EDITMSG |
| Git Common | |
| Git Config | gitconfig .gitconfig .gitmodules |
| Git Ignore | exclude gitignore .gitignore |
| Git Link | .git |
| Git Log | gitlog |
| Git Mailmap | .mailmap mailmap |
| Git Rebase Todo | git-rebase-todo |
| GLSL | vs fs gs vsh fsh gsh vshader fshader gshader vert frag geom tesc tese comp glsl mesh task rgen rint rahit rchit rmiss rcall |
| gnuplot | gp gpl gnuplot gnu plot plt |
| Go | go |
| Gomod | go.mod go.work |
| Gosum | go.sum |
| GraphQL | graphql graphqls gql |
| Graphviz (DOT) | dot DOT gv |
| Groff/troff | groff troff 1 2 3 4 5 6 7 8 9 |
| Groovy | groovy gvy gradle Jenkinsfile |
| group | group |
| Haskell | hs |
| Highlight non-printables | show-nonprintable |
| HTML | html htm shtml xhtml |
| HTML (ASP) | asp |
| HTML (EEx) | html.eex html.leex |
| HTML (Erlang) | yaws |
| HTML (Jinja2) | htm.j2 html.j2 xhtml.j2 xml.j2 |
| HTML (Rails) | rails rhtml erb html.erb |
| HTML (Tcl) | adp |
| HTML (Twig) | twig html.twig |
| HTTP Request and Response | http |
| Idris | idr |
| INI | ini INI inf INF reg REG lng cfg CFG desktop url URL .editorconfig .coveragerc .pylintrc .gitlint .hgrc hgrc |
| Java | java bsh |
| Java Properties | properties |
| Java Server Page (JSP) | jsp |
| Javadoc | |
| JavaScript | js htc |
| JavaScript (Rails) | js.erb |
| Jinja2 | j2 jinja2 jinja |
| JQ | jq |
| JSON | json sublime-settings sublime-menu sublime-keymap sublime-mousemap sublime-theme sublime-build sublime-project sublime-completions sublime-commands sublime-macro sublime-color-scheme ipynb Pipfile.lock |
| JSON (Terraform) | tfstate |
| jsonnet | jsonnet libsonnet libjsonnet |
| Julia | jl |
| Known Hosts | known_hosts known_hosts.old |
| Kotlin | kt kts |
| LaTeX | tex ltx |
| LaTeX Log | |
| Lean 4 | lean |
| Less | less css.less |
| Lisp | lisp cl clisp l mud el scm ss lsp fasl sld |
| Literate Haskell | lhs |
| LiveScript | ls Slakefile ls.erb |
| LLVM | ll |
| log | log |
| Lua | lua |
| Make Output | |
| Makefile | make GNUmakefile makefile Makefile makefile.am Makefile.am makefile.in Makefile.in OCamlMakefile mak mk |
| Manpage | man |
| Markdown | md mdown markdown markdn |
| MATLAB | matlab |
| MediawikerPanel | |
| MediaWiki | mediawiki wikipedia wiki |
| MemInfo | meminfo |
| MultiMarkdown | |
| NAnt Build File | build |
| Navigational Bar SV | |
| Navigational Bar VHDL | |
| nginx | conf.erb conf nginx.conf mime.types fastcgi_params scgi_params uwsgi_params |
| Nim | nim nims nimble |
| Ninja | ninja |
| Nix | nix |
| NSIS | nsi nsh bnsi bnsh nsdinc |
| Objective-C | m h |
| Objective-C++ | mm M h |
| OCaml | ml mli |
| OCamllex | mll |
| OCamlyacc | mly |
| Odin | odin |
| OpenMP (Fortran) | |
| orgmode | org |
| Pascal | pas p dpr |
| passwd | passwd |
| Perl | pl pc pm pmc pod t |
| PHP | php php3 php4 php5 php7 phps phpt phtml |
| PHP Source | |
| Pipe Separated Values | |
| Private Key | |
| Protocol Buffer | proto protobuf protodevel |
| Protocol Buffer (TEXT) | pb.txt proto.text textpb pbtxt prototxt textproto |
| Puppet | pp epp |
| PureScript | purs |
| Python | py py3 pyw pyi pyx pyx.in pxd pxd.in pxi pxi.in rpy cpy SConstruct Sconstruct sconstruct SConscript gyp gypi Snakefile vpy wscript bazel bzl |
| QML | qml qmlproject |
| R | R r Rprofile |
| R Console | |
| Racket | rkt |
| Rd (R Documentation) | rd |
| Rego | rego |
| Regular Expression | re |
| Regular Expressions (Javascript) | |
| Regular Expressions (PHP) | |
| Regular Expressions (Python) | |
| Requirements.txt | requirements.txt requirements.in pip |
| resolv | resolv.conf |
| reStructuredText | rst rest |
| Robot Framework | robot resource |
| Ruby | rb Appfile Appraisals Berksfile Brewfile capfile cgi Cheffile config.ru Deliverfile Fastfile fcgi Gemfile gemspec Guardfile irbrc jbuilder Podfile podspec prawn rabl rake Rakefile Rantfile rbx rjs ruby.rail Scanfile simplecov Snapfile thor Thorfile Vagrantfile |
| Ruby Haml | haml sass |
| Ruby on Rails | rxml builder |
| Ruby Slim | slim skim |
| Rust | rs |
| Sass | sass |
| Scala | scala sbt sc |
| SCSS | scss |
| Semi-Colon Separated Values | |
| Separated Values | csv |
| Shell-Unix-Generic | |
| SML | sml cm sig |
| Solidity | sol |
| SQL | sql ddl dml |
| SQL (Rails) | erbsql sql.erb |
| SSH Common | |
| SSH Config | ssh_config |
| SSH Crypto | |
| SSHD Config | sshd_config |
| Strace | strace |
| Stylus | styl stylus |
| Svelte | svlt svelte |
| Swift | swift |
| syslog | syslog |
| SystemVerilog | sv v svh vh |
| Tab Separated Values | tsv |
| Tcl | tcl |
| Terraform | tf tfvars hcl |
| TestBox | |
| TeX | sty cls |
| Textile | textile |
| Todo.txt | todo.txt done.txt |
| TOML | toml tml Cargo.lock Gopkg.lock Pipfile pdm.lock poetry.lock uv.lock |
| TypeScript | ts mts cts |
| TypeScriptReact | tsx |
| Typst | typ |
| Typst Build Results | |
| Typst Code | |
| varlink | varlink |
| Verilog | v V |
| VHDL | vhd vhdl vho vht |
| VimL | vim vimrc gvimrc .vimrc .gvimrc .exrc .nvimrc _vimrc _gvimrc _exrc |
| Vue Component | vue |
| Vyper | vy |
| WGSL | wgsl |
| x86_64 Assembly | yasm nasm asm inc mac |
| XML | xml xsd xslt tld dtml rng rss opml svg xaml |
| YAML | yaml yml sublime-syntax |
| Zig | zig zon |
Consequences worth knowing: .h files are coloured as C (Emacs's
c-or-c++-mode guesses between C and C++; Kalem does not), .m files
as Objective-C, not MATLAB, as Emacs's auto-mode-alist has it; .conf
and .cfg files as INI. PowerShell has no syntax, so .ps1 files are
not coloured. A file whose extension is the only clue gets no colour
when it has no extension (a file named Makefile or Dockerfile has
no extension and no shebang, and is plain text).
The highlighter gives each span a kind (comment, string, number,
constant, keyword, operator, function, type, variable, tag, macro,
invalid) from its scope; the editors colour kinds with their theme.
After an edit only the changed lines are highlighted again, and the
lines after them until the parser's state is what it was. A file larger
than 4 MiB is highlighted a window at a time: 400 lines around the one
asked for, from a fresh state 200 lines before the window, so a
construct opened further back (a long block comment) may be coloured as
code. A line longer than 16 KiB is shown only in the part around the
cursor, with … for the rest.
4. Comments
Toggle Comment (edit.toggleComment, Ctrl+Alt+C) uses the marker of
the language (code::comment_style), from the same name the mode gives
(the extension, a shebang's program, a mode line's name). It is not
offered in text without a language, in CSV, in folders or in binary
documents.
| Names | Marker |
|---|---|
rs rust c h cc cpp c++ hpp cxx java js javascript jsx ts typescript tsx go swift kt kotlin scala cs csharp dart groovy zig d php json5 jsonc proto glsl hlsl less scss sass v |
// |
py python sh bash zsh fish shell rb ruby pl perl r yaml yml toml make makefile mk cmake conf cfg dockerfile nix jl julia tcl ps1 powershell elixir ex exs nim coffee gitignore awk sed properties env crystal cr graphql |
# |
lua sql hs haskell elm ada adb ads vhdl purs |
-- |
lisp el elisp emacs-lisp clj clojure cljs scm scheme rkt racket fnl fennel ini asm s |
; |
tex latex sty cls bib erl erlang m matlab octave pro prolog ps postscript |
% |
vim vimscript |
" |
f90 f95 fortran |
! |
bat cmd |
REM |
org |
# |
html htm xml xhtml svg md markdown vue plist |
<!-- … --> around each line |
css |
/* … */ around each line |
ml ocaml fs fsharp |
(* … *) around each line |
The lines the selection covers are commented (a selection ending at the start of a line leaves that line out). When every non-blank line already starts with the marker (and, for the enclosing kinds, ends with the closing one), they are uncommented: the marker and one space after it are removed. Otherwise every non-blank line gets the marker and a space at the smallest indentation among them. Blank lines are left alone.
5. Indentation
The indentation step of a document (indent_unit) comes from its text
(text::detect_indent), looking at the first 10,000 lines within the
first megabyte:
- tabs, when more lines start with a tab than with a space;
- otherwise spaces, the step being the most frequent increase of indentation from one non-blank line to the next among 2, 4, 3 and 8 (4 when no increase is seen);
- when no line is indented: tabs for the languages
makefile,make,mkandgo, four spaces for everything else.
Tab and Shift+Tab (in every mode but Org, where Tab folds and moves in tables, and CSV, where they move between fields):
- With a selection spanning lines, Tab adds one step at the start of every non-blank line; Shift+Tab removes one tab, or up to one step of leading spaces, from each line.
- Otherwise Tab inserts a tab, or the spaces up to the next multiple of the step, replacing a selection within the line; Shift+Tab removes one tab, or up to one step of leading spaces, from the start of the line.
Enter (edit.newline, in every mode but Org and LaTeX, which have
their own):
- In a text file with a language (
code::newline), the new line gets the current line's indentation; one step more when the text before the cursor ends with(,[or{(or with:inpy,python,yaml,ymlandnim); and when the cursor sits between a bracket and its partner ({|}), the closing bracket goes to a line of its own at the original indentation, the cursor on an indented line between. - In text without a language, Markdown and CSV (
input::newline), the new line gets the current line's indentation, when the cursor is past it.
Typing }, ) or ] in a text file with a language, where only
indentation precedes the cursor on its line and that indentation ends
with one full step, removes that step first (code::dedent_for).
Go to Matching Bracket (Ctrl+Alt+B) and the marking of the bracket
matching the one at the cursor pair (, [ and { by counting, up to
256 KiB each way; brackets inside strings and comments count like any
other.
Kalem does not convert tabs to spaces or spaces to tabs, and never re-indents lines it did not touch.
6. Encodings
A file's bytes are decoded as follows (files::decode_with):
- A UTF-32 byte order mark (
FF FE 00 00or00 00 FE FF): the file is not opened ("The file is in UTF-32, which Kalem cannot read"), as neither decoding nor saving supports it; it is not mistaken for UTF-16. - A UTF-8 byte order mark: the rest goes through the binary check (see Which mode a file gets) and must be valid UTF-8, or the file is not opened ("The file is not UTF-8 (invalid byte at offset n)"); the mark is removed from the text and remembered.
- A UTF-16 LE or BE byte order mark: the rest is decoded as UTF-16; the mark is remembered.
- Otherwise, after the binary check: valid UTF-8 is UTF-8, unless a
coding:file variable (rule 5) names another encoding. - Otherwise, when the file names its encoding in a
coding:file variable (in the-*-line,-*- coding: latin-5 -*-, or in theLocal Variables:block), that encoding: Emacs's names (latin-1,iso-latin-5,turkish-iso-8bit,utf-8-unix,cp1254) or any WHATWG label, the-unix,-dosand-macsuffixes ignored. - Otherwise the encoding is guessed from the bytes with
chardetng, the way Emacs guesses a coding system, and the status bar says "opened as" that encoding. The guess takes the system locale's region as a hint (tr_TRgivestr), or, with no region, its language or the interface language (tr,jaasjp,elasgr…); so a short Turkish text reads as Windows-1254 on a Turkish system rather than as Windows-1252, which it also fits. When the guessed encoding cannot read every byte, the file is read as Windows-1252, which reads each byte as a character and writes it back as the same byte.
When decoding meets bytes the encoding cannot read (a UTF-16 file with a lone surrogate, or Reopen with Encoding in an encoding that does not fit), they become U+FFFD and the editor says so on opening: saving writes U+FFFD in their place, so such a file does not survive a save unchanged. Save with Encoding accepts that.
The status bar names the encoding when it is not UTF-8. Reopen with
Encoding (file.reopenWithEncoding) reads the file again in the
encoding chosen, whatever its bytes, dropping a byte order mark of that
encoding and turning bytes it cannot read into U+FFFD; it refuses while
the document has unsaved changes. Save with Encoding
(file.saveWithEncoding) makes the document's encoding the one chosen
and saves; UTF-16 is always written with a byte order mark, UTF-8 keeps
the mark if the file had one, other encodings have none. Both offer
UTF-8, UTF-16LE, UTF-16BE, windows-1252, ISO-8859-1, windows-1254,
ISO-8859-9, ISO-8859-15, windows-1250, windows-1251, KOI8-R, Shift_JIS,
EUC-JP, GBK, gb18030, Big5 and EUC-KR, and accept any label the WHATWG
Encoding Standard knows (latin1, utf-16le…). As in that standard
(and in web browsers), ISO-8859-1 is Windows-1252 and ISO-8859-9 is
Windows-1254: the status bar names them so, and bytes 0x80–0x9F are the
Windows characters (€, curly quotes) rather than control codes, which
text files do not use. When a file is
reloaded after another program changed it, the document's encoding is
used again unless a byte order mark names another.
On save the text is encoded in the document's encoding, the byte order
mark written if the file had one. When a character cannot be written in
the encoding (a ł in a Windows-1254 file), the save stops and says
which character and encoding: take the character out, or Save with
Encoding UTF-8, which writes the whole file in UTF-8 from then on.
A file read with replacement characters (bytes that are not in the
encoding chosen, shown as �) is not saved over: Save says so, and
Reopen with Encoding reads it in the right one. Save with Encoding and
Save As write it as it shows, the � included, on purpose.
7. Line endings
The text keeps its line endings as they are in the file; carriage
returns are characters of the text. A document's line ending
(files::line_ending_of) is CR LF when the file has at least one line
feed and every line feed follows a carriage return, as Emacs decides
between dos and unix; CR when the file has carriage returns and no
line feed (Emacs's mac, the files of classic Mac OS); otherwise LF.
- Lines are separated at line feeds. A CR file is the exception to keeping line endings in the text: it is read with every carriage return turned into a line feed, and saved with every line feed turned back into a carriage return, so it has lines and is written as it was. In other files a lone carriage return is not a line break.
- In a CR LF document, commands and typing insert bare line feeds, and
saving writes a carriage return before every line feed that lacks one
(
files::encode). Pasted text has its CR LF made LF first and, in a CR LF document, every line feed made CR LF again. - In an LF document nothing is added: a file with mixed line endings keeps each one as it was, and new lines get LF.
- No final line feed is added or removed.
The setting editor.trim_trailing_whitespace (off by default) removes
spaces and tabs before the end of each line on saving, carriage returns
excepted. It does nothing in CSV files, where trailing tabs are empty
fields and blanks belong to values, nor in Markdown files, where two
trailing spaces are a hard line break.
8. Saving
Saving is atomic: the bytes go to a temporary file beside the target
(.NAME.kalem-PID-N.tmp), which is synced and renamed over it, and the
folder is synced. The file's permissions are kept; a symbolic link is
followed, so the link stays a link and its target is written, and
changes to the target are watched as the link's. The group is kept
when the user belongs to it. A file with several hard links, owned by
another user, or in a folder Kalem may not write in, is written in
place instead, since a rename would detach the links or change the
owner, or cannot be made. A write-protected file is not saved over.
The rename does not keep extended attributes (macOS Finder tags), ACLs
or the creation date; a file written in place keeps them.
With files.backup, the previous file is first copied to NAME.bak.
Before saving, Kalem compares the file with what it last read or wrote (length, time, and a hash of the contents when those differ); when another program changed it, saving stops unless forced. A document without unsaved changes reloads when another program changes its file.
9. Files that are not text
A file found binary (rule 1 of the modes above) is not opened; opening it fails with "The file is not a text file". The graphical editor shows the reason in its status bar (a window opened on such a file shows an empty document with the reason); the terminal editor shows "Cannot open path: The file is not a text file". The file manager's preview of a file with a NUL byte in its first 64 KiB says "Not text: no preview"; pictures are previewed as pictures. Kalem never writes to a file it did not open.
UTF-16 text without a byte order mark contains NUL bytes and is therefore taken for binary; Reopen with Encoding cannot help, since the file is never opened.
10. Known differences and limits
- A file named only by convention (
Makefile,Dockerfile) gets no language. - A shebang is looked at only when the file has no extension.
- Bracket matching and Enter's bracket rules ignore strings and comments.
- Very large files are highlighted in windows and may colour a long construct wrongly.