Repository: asottile/babi

Issue:
utilize identify's shebang detection for grammar when possible
an example:

```python
#!/usr/bin/env python3
print('hello')
```

this file saved as `foo/install` gets highlighted incorrectly as shell, but we can utilize `tags_from_path` to identify this as `python` earlier on

Discussion:
asottile: this still needs a test if you're looking for something to work on

Patch:
diff --git a/babi/buf.py b/babi/buf.py
index 01b385f..0b03d45 100644
--- a/babi/buf.py
+++ b/babi/buf.py
@@ -179,11 +179,6 @@ class Buf:
     def remove_ins_callback(self, cb: InsCallback) -> None:
         self._ins_callbacks.remove(cb)
 
-    def clear_callbacks(self) -> None:
-        self._set_callbacks[:] = [self._set_cb]
-        self._ins_callbacks[:] = [self._ins_cb]
-        self._del_callbacks[:] = [self._del_cb]
-
     @contextlib.contextmanager
     def record(self) -> Generator[list[Modification], None, None]:
         modifications: list[Modification] = []
diff --git a/babi/file.py b/babi/file.py
index 3312d1e..03664b1 100644
--- a/babi/file.py
+++ b/babi/file.py
@@ -286,7 +286,6 @@ class File:
             *file_hls,
             self._trailing_whitespace, self._replace_hl, self.selection,
         )
-        self.buf.clear_callbacks()
         for file_hl in self._file_hls:
             file_hl.register_callbacks(self.buf)
 
@@ -295,10 +294,10 @@ class File:
             hl_factories: tuple[HLFactory, ...],
             color_manager: ColorManager,
     ) -> None:
-        self._trailing_whitespace = TrailingWhitespace(color_manager)
         self._hl_factories = hl_factories
         # only re-initialize the highlighters if we've loaded once
         if self._file_hls:
+            self._trailing_whitespace = TrailingWhitespace(color_manager)
             self._initialize_highlighters()
 
     def __repr__(self) -> str:
diff --git a/babi/highlight.py b/babi/highlight.py
index cbd2f75..284ac53 100644
--- a/babi/highlight.py
+++ b/babi/highlight.py
@@ -10,6 +10,7 @@ from typing import Tuple
 from typing import TypeVar
 
 from identify.identify import tags_from_filename
+from identify.identify import tags_from_path
 
 from babi._types import Protocol
 from babi.fdict import FChainMap
@@ -721,7 +722,11 @@ class Grammars:
         return self.compiler_for_scope('source.unknown')
 
     def compiler_for_file(self, filename: str, first_line: str) -> Compiler:
-        for tag in tags_from_filename(filename) - {'text'}:
+        try:
+            tags = tags_from_path(filename)
+        except ValueError:
+            tags = tags_from_filename(filename)
+        for tag in tags - {'text'}:
             try:
                 # TODO: this doesn't always match even if we detect it
                 return self.compiler_for_scope(f'source.{tag}')


Test patch:
diff --git a/tests/features/retheme_test.py b/tests/features/retheme_test.py
index d0accea..404259c 100644
--- a/tests/features/retheme_test.py
+++ b/tests/features/retheme_test.py
@@ -15,10 +15,7 @@ THEME = json.dumps({
 SYNTAX = json.dumps({
     'scopeName': 'source.demo',
     'fileTypes': ['demo'],
-    'patterns': [
-        {'match': r'#.*$\n?', 'name': 'comment'},
-        {'match': r'-.*$\n?', 'name': 'minus'},
-    ],
+    'patterns': [{'match': r'#.*$\n?', 'name': 'comment'}],
 })
 
 
@@ -106,67 +103,3 @@ def test_retheme_command_multiple_files(run, xdg_config_home, tmpdir):
                     [(20, 40, 0)] * 13 + [(236, 40, 0)] * 27,   # # hello world
             ]):
                 h.assert_screen_attr_equals(i, attr)
-
-
-def test_retheme_bug(run, xdg_config_home, tmpdir):
-    # this tests a complicated theme reloading bug triggered by:
-    # - simple theme with not many colors
-    # - reloads into a more complicated theme
-    # - and then trailing whitespace is introduced
-
-    # at the time of the fix the bug was a leak holding onto the old
-    # highlighters and color manager through callbacks
-
-    def hot_modify_theme():
-        theme_json = json.dumps({
-            'colors': {'background': '#00d700', 'foreground': '#303030'},
-            'tokenColors': [
-                {'scope': 'comment', 'settings': {'foreground': '#c00'}},
-                {'scope': 'minus', 'settings': {'foreground': '#00c'}},
-            ],
-        })
-        xdg_config_home.join('babi/theme.json').write(theme_json)
-
-    f = tmpdir.join('t.demo')
-    f.write('# hello\n- world\n')
-
-    c_rev = [(236, 40, curses.A_REVERSE)]
-    c_base = [(236, 40, 0)]
-    c_comment = [(160, 40, 0)]
-    c_minus = [(20, 40, 0)]
-    c_ws = [(-1, 1, 0)]
-
-    with run(str(f), term='screen-256color', width=80) as h, and_exit(h):
-        h.await_text('# hello\n- world\n')
-
-        for i, attr in enumerate([
-                c_rev * 80,                    # header
-                c_comment * 7 + c_base * 73,   # # hello
-                c_base * 80,                   # - world
-        ]):
-            h.assert_screen_attr_equals(i, attr)
-
-        h.run(hot_modify_theme)
-
-        trigger_command_mode(h)
-        h.press_and_enter(':retheme')
-        h.await_text_missing(':retheme')
-
-        for i, attr in enumerate([
-                c_rev * 80,                    # header
-                c_comment * 7 + c_base * 73,   # # hello
-                c_minus * 7 + c_base * 73,     # - world
-        ]):
-            h.assert_screen_attr_equals(i, attr)
-
-        # trigger trailing whitespace
-        h.press_and_enter('hi ')
-        h.await_text('hi')
-
-        for i, attr in enumerate([
-                c_rev * 80,                           # header
-                c_base * 2 + c_ws * 1 + c_base * 77,  # hi<space>
-                c_comment * 7 + c_base * 73,          # # hello
-                c_minus * 7 + c_base * 73,            # - world
-        ]):
-            h.assert_screen_attr_equals(i, attr)
diff --git a/tests/highlight_test.py b/tests/highlight_test.py
index db8d2b7..3a72e5a 100644
--- a/tests/highlight_test.py
+++ b/tests/highlight_test.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import stat
+
 import pytest
 
 from babi.highlight import highlight_line
@@ -13,6 +15,17 @@ def test_grammar_matches_extension_only_name(make_grammars):
     assert compiler.root_state.entries[0].scope[0] == 'shell'
 
 
+def test_file_without_extension(tmpdir, make_grammars):
+    f = tmpdir.join('f')
+    f.write('#!/usr/bin/env python3')
+    f.chmod(stat.S_IRWXU)
+
+    data = {'scopeName': 'source.python', 'patterns': []}
+    grammars = make_grammars(data)
+    compiler = grammars.compiler_for_file(str(f), f.read())
+    assert compiler.root_state.entries[0].scope[0] == 'source.python'
+
+
 def test_grammar_matches_via_identify_tag(make_grammars):
     grammars = make_grammars({'scopeName': 'source.ini', 'patterns': []})
     compiler = grammars.compiler_for_file('setup.cfg', '')