on master help sign in
Detect filetype from shebang line (#1001)
77dbbc7ath3
book/src/guides/adding_languages.md+2−1
@@ -33,10 +33,11 @@ These are the available keys and descriptions for the file.
33| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
33| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
34| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
34| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
35| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
35| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
36+| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
36| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
37| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
37| auto-format | Whether to autoformat this language when saving |
38| auto-format | Whether to autoformat this language when saving |
38| comment-token | The token to use as a comment-token |
39| comment-token | The token to use as a comment-token |
39−| indent | The indent to use. Has sub keys `tab-width` and `unit` |
40+| indent | The indent to use. Has sub keys `tab-width` and `unit` |
40| config | Language server configuration |
41| config | Language server configuration |
41
42
42## Queries
43## Queries
helix-core/src/indent.rs+1
@@ -450,6 +450,7 @@ where
450 language: vec![LanguageConfiguration {
450 language: vec![LanguageConfiguration {
451 scope: "source.rust".to_string(),
451 scope: "source.rust".to_string(),
452 file_types: vec!["rs".to_string()],
452 file_types: vec!["rs".to_string()],
453+ shebangs: vec![],
453 language_id: "Rust".to_string(),
454 language_id: "Rust".to_string(),
454 highlight_config: OnceCell::new(),
455 highlight_config: OnceCell::new(),
455 config: None,
456 config: None,
helix-core/src/syntax.rs+24
@@ -14,6 +14,8 @@ use std::{
14 cell::RefCell,
14 cell::RefCell,
15 collections::{HashMap, HashSet},
15 collections::{HashMap, HashSet},
16 fmt,
16 fmt,
17+ fs::File,
18+ io::Read,
17 path::Path,
19 path::Path,
18 sync::Arc,
20 sync::Arc,
19};
21};
@@ -52,6 +54,7 @@ pub struct LanguageConfiguration {
52 pub language_id: String,
54 pub language_id: String,
53 pub scope: String, // source.rust
55 pub scope: String, // source.rust
54 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
56 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
57+ pub shebangs: Vec<String>, // interpreter(s) associated with language
55 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
58 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
56 pub comment_token: Option<String>,
59 pub comment_token: Option<String>,
57
60
@@ -254,6 +257,7 @@ pub struct Loader {
254 // highlight_names ?
257 // highlight_names ?
255 language_configs: Vec<Arc<LanguageConfiguration>>,
258 language_configs: Vec<Arc<LanguageConfiguration>>,
256 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
259 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
260+ language_config_ids_by_shebang: HashMap<String, usize>,
257}
261}
258
262
259impl Loader {
263impl Loader {
@@ -261,6 +265,7 @@ impl Loader {
261 let mut loader = Self {
265 let mut loader = Self {
262 language_configs: Vec::new(),
266 language_configs: Vec::new(),
263 language_config_ids_by_file_type: HashMap::new(),
267 language_config_ids_by_file_type: HashMap::new(),
268+ language_config_ids_by_shebang: HashMap::new(),
264 };
269 };
265
270
266 for config in config.language {
271 for config in config.language {
@@ -273,6 +278,11 @@ impl Loader {
273 .language_config_ids_by_file_type
278 .language_config_ids_by_file_type
274 .insert(file_type.clone(), language_id);
279 .insert(file_type.clone(), language_id);
275 }
280 }
281+ for shebang in &config.shebangs {
282+ loader
283+ .language_config_ids_by_shebang
284+ .insert(shebang.clone(), language_id);
285+ }
276
286
277 loader.language_configs.push(Arc::new(config));
287 loader.language_configs.push(Arc::new(config));
278 }
288 }
@@ -298,6 +308,20 @@ impl Loader {
298 // TODO: content_regex handling conflict resolution
308 // TODO: content_regex handling conflict resolution
299 }
309 }
300
310
311+ pub fn language_config_for_shebang(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
312+ // Read the first 128 bytes of the file. If its a shebang line, try to find the language
313+ let file = File::open(path).ok()?;
314+ let mut buf = String::with_capacity(128);
315+ file.take(128).read_to_string(&mut buf).ok()?;
316+ static SHEBANG_REGEX: Lazy<Regex> =
317+ Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
318+ let configuration_id = SHEBANG_REGEX
319+ .captures(&buf)
320+ .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
321+
322+ configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
323+ }
324+
301 pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
325 pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
302 self.language_configs
326 self.language_configs
303 .iter()
327 .iter()
helix-view/src/document.rs+3−1
@@ -494,7 +494,9 @@ impl Document {
494 /// Detect the programming language based on the file type.
494 /// Detect the programming language based on the file type.
495 pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
495 pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
496 if let Some(path) = &self.path {
496 if let Some(path) = &self.path {
497+ let language_config = config_loader
497− let language_config = config_loader.language_config_for_file_name(path);
498+ .language_config_for_file_name(path)
499+ .or_else(|| config_loader.language_config_for_shebang(path));
498 self.set_language(theme, language_config);
500 self.set_language(theme, language_config);
499 }
501 }
500 }
502 }
languages.toml+35
@@ -3,6 +3,7 @@ name = "rust"
3scope = "source.rust"
3scope = "source.rust"
4injection-regex = "rust"
4injection-regex = "rust"
5file-types = ["rs"]
5file-types = ["rs"]
6+shebangs = []
6roots = []
7roots = []
7auto-format = true
8auto-format = true
8comment-token = "//"
9comment-token = "//"
@@ -17,6 +18,7 @@ name = "toml"
17scope = "source.toml"
18scope = "source.toml"
18injection-regex = "toml"
19injection-regex = "toml"
19file-types = ["toml"]
20file-types = ["toml"]
21+shebangs = []
20roots = []
22roots = []
21comment-token = "#"
23comment-token = "#"
22
24
@@ -27,6 +29,7 @@ name = "protobuf"
27scope = "source.proto"
29scope = "source.proto"
28injection-regex = "protobuf"
30injection-regex = "protobuf"
29file-types = ["proto"]
31file-types = ["proto"]
32+shebangs = []
30roots = []
33roots = []
31comment-token = "//"
34comment-token = "//"
32
35
@@ -37,6 +40,7 @@ name = "elixir"
37scope = "source.elixir"
40scope = "source.elixir"
38injection-regex = "elixir"
41injection-regex = "elixir"
39file-types = ["ex", "exs"]
42file-types = ["ex", "exs"]
43+shebangs = []
40roots = []
44roots = []
41comment-token = "#"
45comment-token = "#"
42
46
@@ -48,6 +52,7 @@ name = "mint"
48scope = "source.mint"
52scope = "source.mint"
49injection-regex = "mint"
53injection-regex = "mint"
50file-types = ["mint"]
54file-types = ["mint"]
55+shebangs = []
51roots = []
56roots = []
52comment-token = "//"
57comment-token = "//"
53
58
@@ -59,6 +64,7 @@ name = "json"
59scope = "source.json"
64scope = "source.json"
60injection-regex = "json"
65injection-regex = "json"
61file-types = ["json"]
66file-types = ["json"]
67+shebangs = []
62roots = []
68roots = []
63
69
64indent = { tab-width = 2, unit = " " }
70indent = { tab-width = 2, unit = " " }
@@ -68,6 +74,7 @@ name = "c"
68scope = "source.c"
74scope = "source.c"
69injection-regex = "c"
75injection-regex = "c"
70file-types = ["c"] # TODO: ["h"]
76file-types = ["c"] # TODO: ["h"]
77+shebangs = []
71roots = []
78roots = []
72comment-token = "//"
79comment-token = "//"
73
80
@@ -79,6 +86,7 @@ name = "cpp"
79scope = "source.cpp"
86scope = "source.cpp"
80injection-regex = "cpp"
87injection-regex = "cpp"
81file-types = ["cc", "hh", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"]
88file-types = ["cc", "hh", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"]
89+shebangs = []
82roots = []
90roots = []
83comment-token = "//"
91comment-token = "//"
84
92
@@ -90,6 +98,7 @@ name = "c-sharp"
90scope = "source.csharp"
98scope = "source.csharp"
91injection-regex = "c-?sharp"
99injection-regex = "c-?sharp"
92file-types = ["cs"]
100file-types = ["cs"]
101+shebangs = []
93roots = []
102roots = []
94comment-token = "//"
103comment-token = "//"
95
104
@@ -100,6 +109,7 @@ name = "go"
100scope = "source.go"
109scope = "source.go"
101injection-regex = "go"
110injection-regex = "go"
102file-types = ["go"]
111file-types = ["go"]
112+shebangs = []
103roots = ["Gopkg.toml", "go.mod"]
113roots = ["Gopkg.toml", "go.mod"]
104auto-format = true
114auto-format = true
105comment-token = "//"
115comment-token = "//"
@@ -113,6 +123,7 @@ name = "javascript"
113scope = "source.js"
123scope = "source.js"
114injection-regex = "^(js|javascript)$"
124injection-regex = "^(js|javascript)$"
115file-types = ["js", "mjs"]
125file-types = ["js", "mjs"]
126+shebangs = []
116roots = []
127roots = []
117comment-token = "//"
128comment-token = "//"
118# TODO: highlights-jsx, highlights-params
129# TODO: highlights-jsx, highlights-params
@@ -124,6 +135,7 @@ name = "typescript"
124scope = "source.ts"
135scope = "source.ts"
125injection-regex = "^(ts|typescript)$"
136injection-regex = "^(ts|typescript)$"
126file-types = ["ts"]
137file-types = ["ts"]
138+shebangs = []
127roots = []
139roots = []
128# TODO: highlights-jsx, highlights-params
140# TODO: highlights-jsx, highlights-params
129
141
@@ -135,6 +147,7 @@ name = "tsx"
135scope = "source.tsx"
147scope = "source.tsx"
136injection-regex = "^(tsx)$" # |typescript
148injection-regex = "^(tsx)$" # |typescript
137file-types = ["tsx"]
149file-types = ["tsx"]
150+shebangs = []
138roots = []
151roots = []
139# TODO: highlights-jsx, highlights-params
152# TODO: highlights-jsx, highlights-params
140
153
@@ -146,6 +159,7 @@ name = "css"
146scope = "source.css"
159scope = "source.css"
147injection-regex = "css"
160injection-regex = "css"
148file-types = ["css"]
161file-types = ["css"]
162+shebangs = []
149roots = []
163roots = []
150
164
151indent = { tab-width = 2, unit = " " }
165indent = { tab-width = 2, unit = " " }
@@ -155,6 +169,7 @@ name = "html"
155scope = "text.html.basic"
169scope = "text.html.basic"
156injection-regex = "html"
170injection-regex = "html"
157file-types = ["html"]
171file-types = ["html"]
172+shebangs = []
158roots = []
173roots = []
159
174
160indent = { tab-width = 2, unit = " " }
175indent = { tab-width = 2, unit = " " }
@@ -164,6 +179,7 @@ name = "python"
164scope = "source.python"
179scope = "source.python"
165injection-regex = "python"
180injection-regex = "python"
166file-types = ["py"]
181file-types = ["py"]
182+shebangs = ["python"]
167roots = []
183roots = []
168comment-token = "#"
184comment-token = "#"
169
185
@@ -176,6 +192,7 @@ name = "nix"
176scope = "source.nix"
192scope = "source.nix"
177injection-regex = "nix"
193injection-regex = "nix"
178file-types = ["nix"]
194file-types = ["nix"]
195+shebangs = []
179roots = []
196roots = []
180comment-token = "#"
197comment-token = "#"
181
198
@@ -187,6 +204,7 @@ name = "ruby"
187scope = "source.ruby"
204scope = "source.ruby"
188injection-regex = "ruby"
205injection-regex = "ruby"
189file-types = ["rb"]
206file-types = ["rb"]
207+shebangs = ["ruby"]
190roots = []
208roots = []
191comment-token = "#"
209comment-token = "#"
192
210
@@ -198,6 +216,7 @@ name = "bash"
198scope = "source.bash"
216scope = "source.bash"
199injection-regex = "bash"
217injection-regex = "bash"
200file-types = ["sh", "bash"]
218file-types = ["sh", "bash"]
219+shebangs = ["sh", "bash", "dash"]
201roots = []
220roots = []
202comment-token = "#"
221comment-token = "#"
203
222
@@ -209,6 +228,7 @@ name = "php"
209scope = "source.php"
228scope = "source.php"
210injection-regex = "php"
229injection-regex = "php"
211file-types = ["php"]
230file-types = ["php"]
231+shebangs = ["php"]
212roots = []
232roots = []
213
233
214indent = { tab-width = 4, unit = " " }
234indent = { tab-width = 4, unit = " " }
@@ -218,6 +238,7 @@ name = "latex"
218scope = "source.tex"
238scope = "source.tex"
219injection-regex = "tex"
239injection-regex = "tex"
220file-types = ["tex"]
240file-types = ["tex"]
241+shebangs = []
221roots = []
242roots = []
222comment-token = "%"
243comment-token = "%"
223
244
@@ -228,6 +249,7 @@ name = "julia"
228scope = "source.julia"
249scope = "source.julia"
229injection-regex = "julia"
250injection-regex = "julia"
230file-types = ["jl"]
251file-types = ["jl"]
252+shebangs = []
231roots = []
253roots = []
232comment-token = "#"
254comment-token = "#"
233language-server = { command = "julia", args = [
255language-server = { command = "julia", args = [
@@ -253,6 +275,7 @@ name = "java"
253scope = "source.java"
275scope = "source.java"
254injection-regex = "java"
276injection-regex = "java"
255file-types = ["java"]
277file-types = ["java"]
278+shebangs = []
256roots = []
279roots = []
257indent = { tab-width = 4, unit = " " }
280indent = { tab-width = 4, unit = " " }
258
281
@@ -261,6 +284,7 @@ name = "ledger"
261scope = "source.ledger"
284scope = "source.ledger"
262injection-regex = "ledger"
285injection-regex = "ledger"
263file-types = ["ldg", "ledger", "journal"]
286file-types = ["ldg", "ledger", "journal"]
287+shebangs = []
264roots = []
288roots = []
265comment-token = ";"
289comment-token = ";"
266indent = { tab-width = 4, unit = " " }
290indent = { tab-width = 4, unit = " " }
@@ -270,6 +294,7 @@ name = "ocaml"
270scope = "source.ocaml"
294scope = "source.ocaml"
271injection-regex = "ocaml"
295injection-regex = "ocaml"
272file-types = ["ml"]
296file-types = ["ml"]
297+shebangs = []
273roots = []
298roots = []
274comment-token = "(**)"
299comment-token = "(**)"
275indent = { tab-width = 2, unit = " " }
300indent = { tab-width = 2, unit = " " }
@@ -278,6 +303,7 @@ indent = { tab-width = 2, unit = " " }
278name = "ocaml-interface"
303name = "ocaml-interface"
279scope = "source.ocaml.interface"
304scope = "source.ocaml.interface"
280file-types = ["mli"]
305file-types = ["mli"]
306+shebangs = []
281roots = []
307roots = []
282comment-token = "(**)"
308comment-token = "(**)"
283indent = { tab-width = 2, unit = " "}
309indent = { tab-width = 2, unit = " "}
@@ -286,6 +312,7 @@ indent = { tab-width = 2, unit = " "}
286name = "lua"
312name = "lua"
287scope = "source.lua"
313scope = "source.lua"
288file-types = ["lua"]
314file-types = ["lua"]
315+shebangs = []
289roots = []
316roots = []
290comment-token = "--"
317comment-token = "--"
291indent = { tab-width = 2, unit = " " }
318indent = { tab-width = 2, unit = " " }
@@ -295,6 +322,7 @@ name = "svelte"
295scope = "source.svelte"
322scope = "source.svelte"
296injection-regex = "svelte"
323injection-regex = "svelte"
297file-types = ["svelte"]
324file-types = ["svelte"]
325+shebangs = []
298roots = []
326roots = []
299indent = { tab-width = 2, unit = " " }
327indent = { tab-width = 2, unit = " " }
300language-server = { command = "svelteserver", args = ["--stdio"] }
328language-server = { command = "svelteserver", args = ["--stdio"] }
@@ -305,6 +333,7 @@ name = "vue"
305scope = "source.vue"
333scope = "source.vue"
306injection-regex = "vue"
334injection-regex = "vue"
307file-types = ["vue"]
335file-types = ["vue"]
336+shebangs = []
308roots = []
337roots = []
309indent = { tab-width = 2, unit = " " }
338indent = { tab-width = 2, unit = " " }
310
339
@@ -312,6 +341,7 @@ indent = { tab-width = 2, unit = " " }
312name = "yaml"
341name = "yaml"
313scope = "source.yaml"
342scope = "source.yaml"
314file-types = ["yml", "yaml"]
343file-types = ["yml", "yaml"]
344+shebangs = []
315roots = []
345roots = []
316comment-token = "#"
346comment-token = "#"
317indent = { tab-width = 2, unit = " " }
347indent = { tab-width = 2, unit = " " }
@@ -331,6 +361,7 @@ name = "zig"
331scope = "source.zig"
361scope = "source.zig"
332injection-regex = "zig"
362injection-regex = "zig"
333file-types = ["zig"]
363file-types = ["zig"]
364+shebangs = []
334roots = ["build.zig"]
365roots = ["build.zig"]
335auto-format = true
366auto-format = true
336comment-token = "//"
367comment-token = "//"
@@ -343,6 +374,7 @@ name = "prolog"
343scope = "source.prolog"
374scope = "source.prolog"
344roots = []
375roots = []
345file-types = ["pl", "prolog"]
376file-types = ["pl", "prolog"]
377+shebangs = ["swipl"]
346comment-token = "%"
378comment-token = "%"
347
379
348language-server = { command = "swipl", args = [
380language-server = { command = "swipl", args = [
@@ -354,6 +386,7 @@ language-server = { command = "swipl", args = [
354name = "tsq"
386name = "tsq"
355scope = "source.tsq"
387scope = "source.tsq"
356file-types = ["scm"]
388file-types = ["scm"]
389+shebangs = []
357roots = []
390roots = []
358comment-token = ";"
391comment-token = ";"
359indent = { tab-width = 2, unit = " " }
392indent = { tab-width = 2, unit = " " }
@@ -362,6 +395,7 @@ indent = { tab-width = 2, unit = " " }
362name = "cmake"
395name = "cmake"
363scope = "source.cmake"
396scope = "source.cmake"
364file-types = ["cmake", "CMakeLists.txt"]
397file-types = ["cmake", "CMakeLists.txt"]
398+shebangs = []
365roots = []
399roots = []
366comment-token = "#"
400comment-token = "#"
367indent = { tab-width = 2, unit = " " }
401indent = { tab-width = 2, unit = " " }
@@ -371,6 +405,7 @@ language-server = { command = "cmake-language-server" }
371name = "perl"
405name = "perl"
372scope = "source.perl"
406scope = "source.perl"
373file-types = ["pl", "pm"]
407file-types = ["pl", "pm"]
408+shebangs = ["perl"]
374roots = []
409roots = []
375comment-token = "#"
410comment-token = "#"
376indent = { tab-width = 2, unit = " " }
411indent = { tab-width = 2, unit = " " }