-
Notifications
You must be signed in to change notification settings - Fork 0
fix(heartbeat): add language fallback detection from file extension #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| -- Language detection utilities for shelltime | ||
| -- Provides fallback language detection from file extension when filetype is empty | ||
|
|
||
| local M = {} | ||
|
|
||
| -- File extension to language mapping (used only when filetype is empty) | ||
| local extension_map = { | ||
| lua = 'lua', | ||
| py = 'python', | ||
| js = 'javascript', | ||
| ts = 'typescript', | ||
| tsx = 'typescriptreact', | ||
| jsx = 'javascriptreact', | ||
| rs = 'rust', | ||
| go = 'go', | ||
| rb = 'ruby', | ||
| php = 'php', | ||
| java = 'java', | ||
| kt = 'kotlin', | ||
| swift = 'swift', | ||
| c = 'c', | ||
| cpp = 'cpp', | ||
| cc = 'cpp', | ||
| cxx = 'cpp', | ||
| h = 'c', | ||
| hpp = 'cpp', | ||
| cs = 'csharp', | ||
| sh = 'sh', | ||
| bash = 'bash', | ||
| zsh = 'zsh', | ||
| fish = 'fish', | ||
| html = 'html', | ||
| css = 'css', | ||
| scss = 'scss', | ||
| less = 'less', | ||
| json = 'json', | ||
| yaml = 'yaml', | ||
| yml = 'yaml', | ||
| xml = 'xml', | ||
| md = 'markdown', | ||
| sql = 'sql', | ||
| vim = 'vim', | ||
| dockerfile = 'dockerfile', | ||
| toml = 'toml', | ||
| ini = 'ini', | ||
| conf = 'conf', | ||
| } | ||
|
|
||
| --- Get language for a file | ||
| ---@param filetype string|nil Buffer filetype | ||
| ---@param file_path string File path | ||
| ---@return string Language identifier | ||
| function M.get_language(filetype, file_path) | ||
| -- Use filetype if available | ||
| if filetype and filetype ~= '' then | ||
| return filetype | ||
| end | ||
|
|
||
| -- Fallback: detect from file extension | ||
| local filename = file_path:match('[/\\]?([^/\\]+)$') or '' | ||
|
|
||
| -- Check for hidden files without extension (e.g., .hidden, .gitignore) | ||
| -- These start with a dot and have no other dots | ||
| if filename:match('^%.[^%.]+$') then | ||
| return '' | ||
| end | ||
|
|
||
| local ext = file_path:match('%.([^%.]+)$') | ||
| if ext then | ||
| ext = ext:lower() | ||
| return extension_map[ext] or ext | ||
| end | ||
|
|
||
| -- Special case: Dockerfile, Makefile, etc. | ||
| local basename = filename:lower() | ||
| if basename == 'dockerfile' then | ||
| return 'dockerfile' | ||
| elseif basename == 'makefile' then | ||
| return 'makefile' | ||
| end | ||
|
|
||
| return '' | ||
| end | ||
|
|
||
| return M | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| -- Tests for shelltime/utils/language.lua | ||
| describe('shelltime.utils.language', function() | ||
| local language | ||
|
|
||
| before_each(function() | ||
| package.loaded['shelltime.utils.language'] = nil | ||
| language = require('shelltime.utils.language') | ||
| end) | ||
|
|
||
| describe('get_language', function() | ||
| describe('when filetype is available', function() | ||
| it('should return filetype as-is for lua', function() | ||
| assert.equals('lua', language.get_language('lua', '/path/to/file.lua')) | ||
| end) | ||
|
|
||
| it('should return filetype as-is for python', function() | ||
| assert.equals('python', language.get_language('python', '/path/to/file.py')) | ||
| end) | ||
|
|
||
| it('should return filetype as-is for typescript', function() | ||
| assert.equals('typescript', language.get_language('typescript', '/path/to/file.ts')) | ||
| end) | ||
|
|
||
| it('should return filetype as-is for javascript', function() | ||
| assert.equals('javascript', language.get_language('javascript', '/path/to/file.js')) | ||
| end) | ||
|
|
||
| it('should return filetype as-is for sh', function() | ||
| assert.equals('sh', language.get_language('sh', '/path/to/file.sh')) | ||
| end) | ||
|
|
||
| it('should return filetype as-is for vim', function() | ||
| assert.equals('vim', language.get_language('vim', '/path/to/file.vim')) | ||
| end) | ||
|
|
||
| it('should return filetype even if extension differs', function() | ||
| assert.equals('python', language.get_language('python', '/path/to/file.txt')) | ||
| end) | ||
| end) | ||
|
Comment on lines
+11
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of tests is quite repetitive. You can use a data-driven approach to make the tests more concise and easier to maintain. This involves defining a table of test cases and iterating over it to create the tests. This pattern can also be applied to other repetitive test blocks in this file, such as 'when filetype is empty' and 'special filenames'. |
||
|
|
||
| describe('when filetype is empty', function() | ||
| it('should detect lua from extension', function() | ||
| assert.equals('lua', language.get_language('', '/path/to/file.lua')) | ||
| end) | ||
|
|
||
| it('should detect python from .py extension', function() | ||
| assert.equals('python', language.get_language('', '/path/to/file.py')) | ||
| end) | ||
|
|
||
| it('should detect javascript from .js extension', function() | ||
| assert.equals('javascript', language.get_language('', '/path/to/file.js')) | ||
| end) | ||
|
|
||
| it('should detect typescript from .ts extension', function() | ||
| assert.equals('typescript', language.get_language('', '/path/to/file.ts')) | ||
| end) | ||
|
|
||
| it('should detect typescriptreact from .tsx extension', function() | ||
| assert.equals('typescriptreact', language.get_language('', '/path/to/file.tsx')) | ||
| end) | ||
|
|
||
| it('should detect javascriptreact from .jsx extension', function() | ||
| assert.equals('javascriptreact', language.get_language('', '/path/to/file.jsx')) | ||
| end) | ||
|
|
||
| it('should detect rust from .rs extension', function() | ||
| assert.equals('rust', language.get_language('', '/path/to/file.rs')) | ||
| end) | ||
|
|
||
| it('should detect go from .go extension', function() | ||
| assert.equals('go', language.get_language('', '/path/to/file.go')) | ||
| end) | ||
|
|
||
| it('should detect csharp from .cs extension', function() | ||
| assert.equals('csharp', language.get_language('', '/path/to/file.cs')) | ||
| end) | ||
|
|
||
| it('should detect cpp from .cpp extension', function() | ||
| assert.equals('cpp', language.get_language('', '/path/to/file.cpp')) | ||
| end) | ||
|
|
||
| it('should detect cpp from .cc extension', function() | ||
| assert.equals('cpp', language.get_language('', '/path/to/file.cc')) | ||
| end) | ||
|
|
||
| it('should detect yaml from .yaml extension', function() | ||
| assert.equals('yaml', language.get_language('', '/path/to/file.yaml')) | ||
| end) | ||
|
|
||
| it('should detect yaml from .yml extension', function() | ||
| assert.equals('yaml', language.get_language('', '/path/to/file.yml')) | ||
| end) | ||
|
|
||
| it('should detect markdown from .md extension', function() | ||
| assert.equals('markdown', language.get_language('', '/path/to/file.md')) | ||
| end) | ||
|
|
||
| it('should detect json from .json extension', function() | ||
| assert.equals('json', language.get_language('', '/path/to/file.json')) | ||
| end) | ||
|
|
||
| it('should handle uppercase extensions', function() | ||
| assert.equals('lua', language.get_language('', '/path/to/file.LUA')) | ||
| end) | ||
|
|
||
| it('should handle mixed case extensions', function() | ||
| assert.equals('python', language.get_language('', '/path/to/file.Py')) | ||
| end) | ||
|
|
||
| it('should return unknown extension as-is', function() | ||
| assert.equals('xyz', language.get_language('', '/path/to/file.xyz')) | ||
| end) | ||
| end) | ||
|
|
||
| describe('special filenames', function() | ||
| it('should detect dockerfile', function() | ||
| assert.equals('dockerfile', language.get_language('', '/path/to/Dockerfile')) | ||
| end) | ||
|
|
||
| it('should detect dockerfile case insensitive', function() | ||
| assert.equals('dockerfile', language.get_language('', '/path/to/dockerfile')) | ||
| end) | ||
|
|
||
| it('should detect makefile', function() | ||
| assert.equals('makefile', language.get_language('', '/path/to/Makefile')) | ||
| end) | ||
|
|
||
| it('should detect makefile case insensitive', function() | ||
| assert.equals('makefile', language.get_language('', '/path/to/makefile')) | ||
| end) | ||
| end) | ||
|
|
||
| describe('edge cases', function() | ||
| it('should return empty string for nil filetype and no extension', function() | ||
| assert.equals('', language.get_language(nil, '/path/to/file')) | ||
| end) | ||
|
|
||
| it('should return empty string for empty filetype and no extension', function() | ||
| assert.equals('', language.get_language('', '/path/to/file')) | ||
| end) | ||
|
|
||
| it('should handle file with multiple dots', function() | ||
| assert.equals('lua', language.get_language('', '/path/to/file.test.lua')) | ||
| end) | ||
|
|
||
| it('should handle hidden files with extension', function() | ||
| assert.equals('yaml', language.get_language('', '/path/to/.hidden.yaml')) | ||
| end) | ||
|
|
||
| it('should return empty for hidden files without extension', function() | ||
| assert.equals('', language.get_language('', '/path/to/.hidden')) | ||
| end) | ||
|
|
||
| it('should handle nil filetype', function() | ||
| assert.equals('lua', language.get_language(nil, '/path/to/file.lua')) | ||
| end) | ||
|
|
||
| it('should prefer filetype over extension', function() | ||
| assert.equals('typescriptreact', language.get_language('typescriptreact', '/path/to/file.tsx')) | ||
| end) | ||
| end) | ||
| end) | ||
| end) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The special filename handling can be made more maintainable and scalable by using a lookup table instead of an
if/elseifchain. This makes it easier to add more special filenames in the future.For better performance, you could also consider moving the
special_filenamestable outside theget_languagefunction to avoid re-creating it on every call.