Open NikitaRevenco opened 3 months ago
Windows has "File Associations". That's how e.g. image.png
opens in whatever app is registered to handle the .PNG extension. Read about File Associations in Windows for more info.
But I don't think File Associations apply to URLs the way you want in the two URL examples you gave.
In a Lua script loaded into Clink, you can do any arbitrary preprocessing of the input line before it gets passed to CMD via clink.onfilterinput(). You could do your own processing to give the effect you're looking for.
But watch out for edge cases. E.g. foo.git
should NOT expand if there's a file foo.git.exe
along the system %PATH%. (Or foo.git.bat, or any other extension listed in %PATHEXT%.)
Windows has "File Associations". That's how e.g.
image.png
opens in whatever app is registered to handle the .PNG extension. Read about File Associations in Windows for more info.But I don't think File Associations apply to URLs the way you want in the two URL examples you gave.
In a Lua script loaded into Clink, you can do any arbitrary preprocessing of the input line before it gets passed to CMD via clink.onfilterinput(). You could do your own processing to give the effect you're looking for.
But watch out for edge cases. E.g.
foo.git
should NOT expand if there's a filefoo.git.exe
along the system %PATH%. (Or foo.git.bat, or any other extension listed in %PATHEXT%.)
Thank you! 😃 It didn't turn out to be very complicated to make, which is nice
local suffix_aliases = {
[".git"] = "git clone",
}
local function file_exists_in_directory(directory, filename)
local separator = "\\"
local path = directory .. separator .. filename
local file = io.open(path, "r")
if file then
file:close()
return true
end
return false
end
local function is_in_path(program)
local path_env = os.getenv("PATH")
local path_ext = os.getenv("PATHEXT")
if not path_ext then
error("%PATHEXT% is not available")
elseif not path_env then
error("%PATH% is not available")
end
local extensions = {}
for str in string.gmatch(path_ext, "([^;]+)") do
table.insert(extensions, str)
end
local split_path = string.gmatch(path_env, "[^;]+")
for dir in split_path do
for _, ext in ipairs(extensions) do
if file_exists_in_directory(dir, program .. ext) then
return true
end
end
end
return false
end
local function string_endswith(string, suffix)
return string:sub(-#suffix) == suffix
end
local function onfilterinput(text)
for suffix, command in pairs(suffix_aliases) do
if string_endswith(text, suffix) then
local text_without_suffix = string.sub(text, 0, #text - #suffix)
if not is_in_path(text_without_suffix) then
return command .. " " .. text
end
end
end
end
if clink.onfilterinput then
clink.onfilterinput(onfilterinput)
else
clink.onendedit(onfilterinput)
end
I can now create as many suffixes as I want!
I would like the suffix to also be highlighted with clink.argmatcher()
when it's a valid suffix and i tried this:
local function onfilterinput(text)
for suffix, command in pairs(suffix_aliases) do
if string_endswith(text, suffix) then
+ clink.argmatcher(text)
local text_without_suffix = string.sub(text, 0, #text - #suffix)
if not is_in_path(text_without_suffix) then
return command .. " " .. text
end
end
end
end
Since now the entire text
is in argmatcher, I was expecting it to highlight everything with color.argmatcher
but it didn't, do you have any advice?
also how about adding this to the repository?
I would like the suffix to also be highlighted with
clink.argmatcher()
when it's a valid suffix and i tried this:local function onfilterinput(text) for suffix, command in pairs(suffix_aliases) do if string_endswith(text, suffix) then + clink.argmatcher(text) local text_without_suffix = string.sub(text, 0, #text - #suffix) if not is_in_path(text_without_suffix) then return command .. " " .. text end end end end
Since now the entire
text
is in argmatcher, I was expecting it to highlight everything withcolor.argmatcher
but it didn't, do you have any advice?
There are some problems with that:
clink.argmatcher(text)
creates a blank argmatcher that does nothing at all.color.argmatcher
would be misleading since this has nothing to do with argmatchers and is incompatible with any of their behaviors. Consider creating a new color setting instead (or maybe use color.doskey
, but that may also be misleading).If you want to apply custom coloring to the input line, then refer to Setting a classifier function for the whole input line.
Also, the text
variable contains the entire command line. The way the code is written, when text
is echo foo bar address.git
the code turns it into git clone echo foo bar address.git
. Was that intended? It seems undesirable.
also how about adding this to the repository?
This isn't ready to be included in a repo:
echo foo bar address.git
example might be an issue.Also, if it's included in the clink-gizmos repo, then that implies I would take over maintenance of it. I'm not sure I want to do that.
It seems like something that would be good to distribute separately, e.g. in separate repo that you maintain. If you create a repo, let me know and I'd be happy to include a link to it in the Clink documentation, along with the links to other script repos.
More thoughts:
If you hook up a clink.classifier()
to do input line coloring, then there's an edge case to consider: The call to is_in_path()
is a blocking call and can take a while, depending on text (especially since it's looping over all suffixes). Classifiers need to be very fast, otherwise they'll introduce lag while typing and/or updating the display. To ensure high performance, it would need to use os.getdrivetype()
and check for remote drives before calling file_exists_in_directory()
.
Using io.open()
in file_exists_in_directory()
is an inefficient way to check for file existence, especially if the file is on a remote drive. Use os.isfile()
instead.
Is looping over the list of suffixes necessary? That will get slower the more suffixes are defined. It's already set up as an associative table with suffixes as keys. The loop could be eliminated by getting the suffix from the line and indexing into the table.
local function onfilterinput(text)
local suffix = text:match("(%.[^. /\\]+)$")
if suffix then
local command = suffix_aliases[suffix]
if command then
return command .. " " .. text
end
end
end
[!WARNING] The example above is not trying to fix the issue where
echo foo bar repo.git
becomegit clone echo foo bar repo.git
. Fixing that requires more sophisticate parsing oftext
.
Also, what about an input line echo foo & repo.git
, or an input line repo.git & echo done
?
You can use clink.parseline(text) to get a table of line_state objects for each of the commands in the text
input line, and then apply suffix alias expansion to each command.
Also, what about an input line
echo foo & repo.git
, or an input linerepo.git & echo done
?You can use clink.parseline(text) to get a table of line_state objects for each of the commands in the
text
input line, and then apply suffix alias expansion to each command.
Hey, thank you for the insightful responses! I updated the color matching to use a custom user config theme.
After thinking about it for a bit, I'm not sure if the added complexity and performance costs are really worth command like cd.git
not being expanded.
Since I was basing this suggestion off of zsh, I think it makes sense to implement it how they exactly have it. This has the added benefit that people can transfer their ZSH suffix aliases directly into clink
So the way it works in zsh is that it will iterate through every single word in the input line and expand them individually. For example echo foo bar.git
will become echo foo bar git clone bar.git
, and zsh doesn't account if something can be an executable. For example cd.git
will just expand to git clone cd.git
Here's the current code:
settings.add("color.suffix", "", "Color for when suffix is matched")
local suffixes = {
[".git"] = "git clone",
}
local function escape_pattern(str)
return str:gsub("(%W)", "%%%1")
end
local pattern_parts = {}
for suffix, _ in pairs(suffixes) do
table.insert(pattern_parts, escape_pattern(suffix) .. "$")
end
local combined_pattern = table.concat(pattern_parts, "|")
local suffix_classifier = clink.classifier(1)
function suffix_classifier:classify(commands)
local line = commands[1].line_state:getline()
local color = settings.get("color.suffix") or ""
local classifications = commands[1].classifications
local last_index = 1
for word in line:gmatch("%S+") do
local match = word:match(combined_pattern)
local start_index, end_index = string.find(line, word, last_index, true)
last_index = end_index + 1
if match and suffixes[match] then
classifications:applycolor(start_index, end_index - start_index + 1, color)
end
end
end
local function onfilterinput(line)
local words = {}
for word in line:gmatch("%S+") do
local match = word:match(combined_pattern)
if match and suffixes[match] then
word = suffixes[match] .. " " .. word
end
table.insert(words, word)
end
return table.concat(words, " ")
end
if clink.onfilterinput then
clink.onfilterinput(onfilterinput)
else
clink.onendedit(onfilterinput)
end
I wanted to also allow the user to manually specify suffix aliases. I wasn't sure how to do it the way I really envisioned how its gonna work:
suffix
suffix..git = git clone
suffix.!random123 = foo
And then I can iterate through the "suffix" setting in my code and check each property.
But I don't think this is possible, so instead I tried the following: (I replace the commented out part with the non-commented out part.)
settings.add("suffix.aliases", ".js=nvim;.git=git clone;", "Suffix aliases")
local suffixes = {}
for suffix_prepend_pair in string.gmatch(settings.get("suffix.aliases"), "([^;]+)") do
local suffix = {}
for suffix_or_prepend in string.gmatch(suffix_prepend_pair, "([^=]+)") do
table.insert(suffix, suffix_or_prepend)
end
suffixes[suffix[1]] = suffix[2]
end
-- local suffixes = {
-- [".git"] = "git clone",
-- [".js"] = "nvim",
-- }
For some reason, the script no longer works at all. I'm not sure why this is happening. I used serializeTable
function i found from this stack overflow post so that I can debug the suffixes
table. In both cases (with the commented out parts switched around) the suffixes
table had exactly the same structure:
{
.git = git clone,
.js = nvim,
}
However with the new approach, where I add the suffix.aliases
setting, it just stops working. The code is not asynchronous (I don't think...?) so this doesn't make sense to me. The structure of suffixes
is identical, so why it suddenly stops working?
If you don't want to add it to the repo I can understand that. i can add documentation and publish a repo once I'm able to figure out how I can let users define their suffix alias in clink_settings
(by the way I tried implementing your idea where we look at each word and then check its ending. The problem with that is users can define any suffix alias, it doesn't have to start with a period)
So the way it works in zsh is that it will iterate through every single word in the input line and expand them individually. For example
echo foo bar.git
will becomeecho foo bar git clone bar.git
, and zsh doesn't account if something can be an executable. For examplecd.git
will just expand togit clone cd.git
Ah, ok, that makes it simpler and more consistent. This is sounding promising...
Here's the current code:
The zsh parity approach sounds interesting, and I'll load the script and share my observations after I've examined it a bit.
This has the added benefit that people can transfer their ZSH suffix aliases directly into clink ... I wanted to also allow the user to manually specify suffix aliases. I wasn't sure how to do it the way I really envisioned how its gonna work:
How does one specify them in zsh?
.inputrc
file, if you have one.suffix-alias suffix=expansion
, similar to how clink-flex-prompt implements a custom flexprompt configure
command.
- I define a new setting
suffix
And then I can iterate through the "suffix" setting in my code and check each property.
But I don't think this is possible
I agree that using multiple settings wouldn't result in a nice user experience. Trying to use multiple suffix.whatever
settings would be problematic because settings in Clink are not meant to be dynamically created by users. There's no way to dynamically delete a setting that's been created, for example.
so instead I tried the following: (I replace the commented out part with the non-commented out part.)
settings.add("suffix.aliases", ".js=nvim;.git=git clone;", "Suffix aliases") local suffixes = {} for suffix_prepend_pair in string.gmatch(settings.get("suffix.aliases"), "([^;]+)") do local suffix = {} for suffix_or_prepend in string.gmatch(suffix_prepend_pair, "([^=]+)") do table.insert(suffix, suffix_or_prepend) end suffixes[suffix[1]] = suffix[2] end -- local suffixes = { -- [".git"] = "git clone", -- [".js"] = "nvim", -- }
For some reason, the script no longer works at all. I'm not sure why this is happening.
Re: "no longer works at all" -- But what does happen? I can see a few ways to improve the code, but I don't see anything that would make it "no longer work at all". I do see that if you try to use clink set suffix.aliases new_list_of_suffix_aliases
then it won't update to use the new list, because the Lua script only parses the setting when the script is loaded, which only happens when starting Clink or when forcing a reload e.g. via Ctrl-X,Ctrl-R.
If you want to debug it, you could run clink set lua.debug true
and then add a line pause()
at the spot where you want to break into the debugger and start debugging. Type help
at the debugger prompt for available commands.
I used
serializeTable
function i found from this stack overflow post so that I can debug thesuffixes
table.
You could use dumpvar()
which is part of clink-gizmos (and is probably richer than what's in the stackoverflow post you found).
In the Lua debugger, you could use dump suffixes
to show the table contents, or you could use dumpvar(suffixes)
(since you have clink-gizmos loaded already and the Lua debugger lets you run any arbitrary Lua code at the debugger prompt).
If you don't want to add it to the repo I can understand that. i can add documentation and publish a repo once I'm able to figure out how I can let users define their suffix alias in
clink_settings
With the additional info and simplification to match how zsh works, it's something I might be willing to add into clink-gizmos.
(by the way I tried implementing your idea where we look at each word and then check its ending. The problem with that is users can define any suffix alias, it doesn't have to start with a period)
Got it. Without optimizations, that will begin to perform poorly as the number of suffix definitions grows. To optimize for that, I would probably use a data structure like a Trie to enable fast matching in reverse starting from the end of a string (e.g. implement a sparse Trie using Lua associative tables).
I found the bug.
settings.add("suffix.aliases", ".js=nvim;.git=git clone;", "Suffix aliases") local suffixes = {} for suffix_prepend_pair in string.gmatch(settings.get("suffix.aliases"), "([^;]+)") do local suffix = {} for suffix_or_prepend in string.gmatch(suffix_prepend_pair, "([^=]+)") do table.insert(suffix, suffix_or_prepend) end suffixes[suffix[1]] = suffix[2] end -- local suffixes = { -- [".git"] = "git clone", -- [".js"] = "nvim", -- }
For some reason, the script no longer works at all. I'm not sure why this is happening. I used
serializeTable
function i found from this stack overflow post so that I can debug thesuffixes
table. In both cases (with the commented out parts switched around) thesuffixes
table had exactly the same structure:
It has nothing to do with the table structure.
If you restore the commented-out code, then it will also not work. Any time there is more than one suffix defined, the code won't work. That's the difference that made it stop working.
Because the code tries to construct a regular expression string instead of a Lua pattern string.
Lua patterns do not have a |
operator.
But if Lua did have regular expressions, then the code would be constructing a regular expression whose size and complexity is proportional to the number of suffixes defined. That could work (if Lua had regular expressions), but its performance wouldn't scale very well as the number of suffixes grows.
The performance almost might not matter much if it only occurs when pressing Enter. But performance matters for input line coloring.
The first thing to do is to get the basics working, and configurability (e.g. reading a file of suffix alias definitions).
So for starters, looping over pairs(suffixes)
would work and would be a good start. Maybe not ready for including in clink-gizmos, but probably plenty good enough for your own purposes.
I found the bug.
settings.add("suffix.aliases", ".js=nvim;.git=git clone;", "Suffix aliases") local suffixes = {} for suffix_prepend_pair in string.gmatch(settings.get("suffix.aliases"), "([^;]+)") do local suffix = {} for suffix_or_prepend in string.gmatch(suffix_prepend_pair, "([^=]+)") do table.insert(suffix, suffix_or_prepend) end suffixes[suffix[1]] = suffix[2] end -- local suffixes = { -- [".git"] = "git clone", -- [".js"] = "nvim", -- }
For some reason, the script no longer works at all. I'm not sure why this is happening. I used
serializeTable
function i found from this stack overflow post so that I can debug thesuffixes
table. In both cases (with the commented out parts switched around) thesuffixes
table had exactly the same structure:The bug
It has nothing to do with the table structure.
If you restore the commented-out code, then it will also not work. Any time there is more than one suffix defined, the code won't work. That's the difference that made it stop working.
Because the code tries to construct a regular expression string instead of a Lua pattern string. Lua patterns do not have a
|
operator.Performance
But if Lua did have regular expressions, then the code would be constructing a regular expression whose size and complexity is proportional to the number of suffixes defined. That could work (if Lua had regular expressions), but its performance wouldn't scale very well as the number of suffixes grows.
The performance almost might not matter much if it only occurs when pressing Enter. But performance matters for input line coloring.
But, optimizations can come later
The first thing to do is to get the basics working, and configurability (e.g. reading a file of suffix alias definitions).
So for starters, looping over
pairs(suffixes)
would work and would be a good start. Maybe not ready for including in clink-gizmos, but probably plenty good enough for your own purposes.
thank you! I ended up replacing it with this instead:
local function get_suffix(word)
for suffix in pairs(suffixes) do
local pattern = escape_pattern(suffix) .. "$"
if word:match(pattern) then
return suffix
end
end
return false
end
Not sure if this will be faster than the loop method, but we'll see.
The way zsh allows for suffix aliases is very simple, it just adds a -s
flag to the alias
command. alias
is similar to doskey
For example we can have a file, my-alias.sh
alias -s .git="git clone"
And then when I make it executable and run it by typing its name, I'll have the .git
suffix alias available to use. I can also just directly type the command, and I can put the line in any of my scripts which execute on startup
So to allow user customizability right now we may have two options:
Current script:
settings.add("color.suffix", "", "Color for when suffix is matched")
settings.add("suffix.aliases", ".git=git clone;", "Suffix aliases")
local suffixes = {}
for suffix_prepend_pair in string.gmatch(settings.get("suffix.aliases"), "([^;]+)") do
local suffix = {}
for suffix_or_prepend in string.gmatch(suffix_prepend_pair, "([^=]+)") do
table.insert(suffix, suffix_or_prepend)
end
suffixes[suffix[1]] = suffix[2]
end
local function escape_pattern(str)
return str:gsub("(%W)", "%%%1")
end
local function get_suffix(word)
for suffix in pairs(suffixes) do
local pattern = escape_pattern(suffix) .. "$"
if word:match(pattern) then
return suffix
end
end
return false
end
local suffix_classifier = clink.classifier(1)
function suffix_classifier:classify(commands)
local line = commands[1].line_state:getline()
local color = settings.get("color.suffix") or ""
local classifications = commands[1].classifications
local last_index = 1
for word in line:gmatch("%S+") do
local match = get_suffix(word)
local start_index, end_index = string.find(line, word, last_index, true)
last_index = end_index + 1
if match and suffixes[match] then
classifications:applycolor(start_index, end_index - start_index + 1, color)
end
end
end
local function onfilterinput(line)
local words = {}
for word in line:gmatch("%S+") do
local match = get_suffix(word)
if match and suffixes[match] then
word = suffixes[match] .. " " .. word
end
table.insert(words, word)
end
return table.concat(words, " ")
end
if clink.onfilterinput then
clink.onfilterinput(onfilterinput)
else
clink.onendedit(onfilterinput)
end
As written, the onfilterinput
function is replacing all multi-space sequences with a single space. That makes it impossible to enter certain kinds of commands. That's one of the reasons it can be very dangerous to do onfilterinput
processing.
It also doesn't respect quotes.
I have a version with several changes and bug fixes, which I'll share soon.
This fixes some subtle yet important issues, such as:
It also improves efficiency a little bit, but it doesn't implement a Trie yet. I added some "FUTURE" comments for where/how further enhancements could be made.
It seems to work for me, but I haven't tested it extensively.
[!NOTE] I added a
DEBUG_SUFFIX_ALIASES
environment variable which can be set to only print the expansion without actually returning it to CMD. E.g.set DEBUG_SUFFIX_ALIASES=1
.
if not clink.parseline then
log.info("suffix_aliases.lua requires a newer version of Clink; please upgrade.")
return
end
settings.add("color.suffix_alias", "bri mag", "Color when a suffix alias is matched")
settings.add("suffix.aliases", ".js=nvim;.git=git clone", "List of suffix aliases")
local suffixes = {}
--------------------------------------------------------------------------------
-- Ensure that suffixes are loaded.
--
-- For now this just loads from a setting, any time the setting changes.
--
-- Eventually it could reload from a file, for example whenever the file
-- timestamp changes.
local prev_suffixes
local function ensure_suffixes()
-- FUTURE: To load from a file, this could check the timestamp and compare
-- it to a prev_timestamp value, and bail out if they match.
local s = settings.get("suffix.aliases")
if s == prev_suffixes then
return
end
prev_suffixes = s
-- FUTURE: To load from a file, this could be replaced with f = io.open()
-- and a for loop over f:lines().
for _,entry in ipairs(string.explode(s, ";")) do
local name,value = entry:match("^%s*([^=]-)%s*=%s*(.*)%s*$")
if name then
suffixes[name] = value
elseif entry ~= "" and entry:gsub(" ", "") ~= "" then
log.info(string.format("Unable to parse '%s' in setting suffix.aliases.", entry))
end
end
-- FUTURE: This is where to build a sparse Trie data structure (e.g. using
-- Lua tables with string.byte() values as keys) that can optimize matching
-- suffixes starting from the end of a word. To reduce construction time
-- and memory consumption, it could probably use a Trie for only the last 5
-- to 10 characters, and then switch to looping over substrings.
--
-- E.g. this illustrates the a sparse Trie that switches to an array of
-- substrings after the last 2 characters:
--
-- {
-- [string.byte("t")] =
-- {
-- [string.byte("i")] =
-- {
-- [".g"] = "git clone",
-- },
-- },
-- [string.byte("d")] =
-- {
-- [string.byte("l")] =
-- {
-- ["wor"] = "echo hello",
-- ["fo"] = "echo poker",
-- },
-- [string.byte("e")] =
-- {
-- ["r"] = "echo color",
-- },
-- },
-- }
end
clink.onbeginedit(ensure_suffixes)
--------------------------------------------------------------------------------
-- Look up a suffix alias for a word.
local function get_suffix_alias(word)
local wordlen = #word
if wordlen > 0 then
-- FUTURE: Performance scales poorly as the number of defined suffixes
-- grows. That could be improved with a sparse Trie data structure that
-- matches characters in reverse order from the end of the word.
for name,value in pairs(suffixes) do
local namelen = #name
if wordlen >= namelen and string.matchlen(word:sub(wordlen + 1 - namelen), name) == -1 then
return value, name
end
end
end
end
--------------------------------------------------------------------------------
-- Do input line coloring.
local suffix_classifier = clink.classifier(1)
function suffix_classifier:classify(commands)
-- Do nothing if no color is set.
local color = settings.get("color.suffix_alias") or ""
if color == "" then
return
end
-- Loop through the commands in the input line and apply coloring to each.
for _,command in ipairs(commands) do
local line_state = command.line_state
local classifications = command.classifications
for i = 1, line_state:getwordcount() do
local word = line_state:getword(i)
if get_suffix_alias(word) then
local info = line_state:getwordinfo(i)
classifications:applycolor(info.offset, info.length, color)
end
end
end
end
--------------------------------------------------------------------------------
-- Provide an input hint (requires Clink v1.7.0 or newer).
-- The input hint is displayed below the input line, in the comment row (like
-- where history.show_preview displays history expansion previews).
local suffix_hinter = clink.hinter and clink.hinter(1) or {}
function suffix_hinter:gethint(line_state)
local cursorpos = line_state:getcursor()
for i = line_state:getwordcount(), 1, -1 do
local info = line_state:getwordinfo(i)
if cursorpos > info.offset + info.length then
return
elseif info.offset <= cursorpos and cursorpos <= info.offset + info.length then
local word = line_state:getword(i)
local value, suffix = get_suffix_alias(word)
if value then
local hint = string.format("%s=%s", suffix, value)
if (settings.get("color.suffix_alias") or "") == "" then
hint = "Suffix alias "..hint
end
return hint, info.offset
end
return
end
end
end
--------------------------------------------------------------------------------
-- Insert suffix commands before any word that has a matching suffix alias.
local function onfilterinput(line)
local last = 0
local parts = {}
local commands = clink.parseline(line)
for _,command in ipairs(commands) do
local line_state = command.line_state
for i = 1, line_state:getwordcount() do
local word = line_state:getword(i)
local value = get_suffix_alias(word)
if value then
local info = line_state:getwordinfo(i)
local next
if info.quoted then
next = info.offset - 1
else
next = info.offset
end
table.insert(parts, line:sub(last + 1, next - 1))
table.insert(parts, value.." ")
last = next - 1
end
end
end
if parts[1] then
table.insert(parts, line:sub(last + 1, #line))
local result = table.concat(parts, "")
if os.getenv("DEBUG_SUFFIX_ALIASES") then
print("SUFFIX ALIAS EXPANSION:")
print(result)
else
line = result
end
end
return line
end
clink.onfilterinput(onfilterinput)
This fixes some subtle yet important issues, such as:
* Didn't respect quotes. * Converted all runs of 2 or more spaces into a 1 space. * Didn't update when the suffix.aliases setting gets changed.
It also improves efficiency a little bit, but it doesn't implement a Trie yet. I added some "FUTURE" comments for where/how further enhancements could be made.
It seems to work for me, but I haven't tested it extensively.
Note
I added a
DEBUG_SUFFIX_ALIASES
environment variable which can be set to only print the expansion without actually returning it to CMD. E.g.set DEBUG_SUFFIX_ALIASES=1
.if not clink.parseline then log.info("suffix_aliases.lua requires a newer version of Clink; please upgrade.") return end settings.add("color.suffix_alias", "bri mag", "Color when a suffix alias is matched") settings.add("suffix.aliases", ".js=nvim;.git=git clone", "List of suffix aliases") local suffixes = {} -------------------------------------------------------------------------------- -- Ensure that suffixes are loaded. -- -- For now this just loads from a setting, any time the setting changes. -- -- Eventually it could reload from a file, for example whenever the file -- timestamp changes. local prev_suffixes local function ensure_suffixes() -- FUTURE: To load from a file, this could check the timestamp and compare -- it to a prev_timestamp value, and bail out if they match. local s = settings.get("suffix.aliases") if s == prev_suffixes then return end prev_suffixes = s -- FUTURE: To load from a file, this could be replaced with f = io.open() -- and a for loop over f:lines(). for _,entry in ipairs(string.explode(s, ";")) do local name,value = entry:match("^%s*([^=]-)%s*=%s*(.*)%s*$") if name then suffixes[name] = value elseif entry ~= "" and entry:gsub(" ", "") ~= "" then log.info(string.format("Unable to parse '%s' in setting suffix.aliases.", entry)) end end -- FUTURE: This is where to build a sparse Trie data structure (e.g. using -- Lua tables with string.byte() values as keys) that can optimize matching -- suffixes starting from the end of a word. To reduce construction time -- and memory consumption, it could probably use a Trie for only the last 5 -- to 10 characters, and then switch to looping over substrings. -- -- E.g. this illustrates the a sparse Trie that switches to an array of -- substrings after the last 2 characters: -- -- { -- [string.byte("t")] = -- { -- [string.byte("i")] = -- { -- [".g"] = "git clone", -- }, -- }, -- [string.byte("d")] = -- { -- [string.byte("l")] = -- { -- ["wor"] = "echo hello", -- ["fo"] = "echo poker", -- }, -- [string.byte("e")] = -- { -- ["r"] = "echo color", -- }, -- }, -- } end clink.onbeginedit(ensure_suffixes) -------------------------------------------------------------------------------- -- Look up a suffix alias for a word. local function get_suffix_alias(word) local wordlen = #word if wordlen > 0 then -- FUTURE: Performance scales poorly as the number of defined suffixes -- grows. That could be improved with a sparse Trie data structure that -- matches characters in reverse order from the end of the word. for name,value in pairs(suffixes) do local namelen = #name if wordlen >= namelen and string.matchlen(word:sub(wordlen + 1 - namelen), name) == -1 then return value, name end end end end -------------------------------------------------------------------------------- -- Do input line coloring. local suffix_classifier = clink.classifier(1) function suffix_classifier:classify(commands) -- Do nothing if no color is set. local color = settings.get("color.suffix_alias") or "" if color == "" then return end -- Loop through the commands in the input line and apply coloring to each. for _,command in ipairs(commands) do local line_state = command.line_state local classifications = command.classifications for i = 1, line_state:getwordcount() do local word = line_state:getword(i) if get_suffix_alias(word) then local info = line_state:getwordinfo(i) classifications:applycolor(info.offset, info.length, color) end end end end -------------------------------------------------------------------------------- -- Provide an input hint (requires Clink v1.7.0 or newer). -- The input hint is displayed below the input line, in the comment row (like -- where history.show_preview displays history expansion previews). local suffix_hinter = clink.hinter and clink.hinter(1) or {} function suffix_hinter:gethint(line_state) local cursorpos = line_state:getcursor() for i = line_state:getwordcount(), 1, -1 do local info = line_state:getwordinfo(i) if cursorpos > info.offset + info.length then return elseif info.offset <= cursorpos and cursorpos <= info.offset + info.length then local word = line_state:getword(i) local value, suffix = get_suffix_alias(word) if value then local hint = string.format("%s=%s", suffix, value) if (settings.get("color.suffix_alias") or "") == "" then hint = "Suffix alias "..hint end return hint, info.offset end return end end end -------------------------------------------------------------------------------- -- Insert suffix commands before any word that has a matching suffix alias. local function onfilterinput(line) local last = 0 local parts = {} local commands = clink.parseline(line) for _,command in ipairs(commands) do local line_state = command.line_state for i = 1, line_state:getwordcount() do local word = line_state:getword(i) local value = get_suffix_alias(word) if value then local info = line_state:getwordinfo(i) local next if info.quoted then next = info.offset - 1 else next = info.offset end table.insert(parts, line:sub(last + 1, next - 1)) table.insert(parts, value.." ") last = next - 1 end end end if parts[1] then table.insert(parts, line:sub(last + 1, #line)) local result = table.concat(parts, "") if os.getenv("DEBUG_SUFFIX_ALIASES") then print("SUFFIX ALIAS EXPANSION:") print(result) else line = result end end return line end clink.onfilterinput(onfilterinput)
this is really awesome, thank you!
Hey, I was looking for a while if there is some way I can have a "suffix alias" which are available in unix shell like zsh? For example, we can set an alias like below:
And then all of these commands:
Would become expand to these when we press enter:
This is useful for other scenarios, for example I could type "image.png" into my terminal and it'll open it with my image editor instead of having to type like "paintdotnet image.png"
If there is some function in clink that allows me get the contents of the prompt and then substitute it with a different command before it will be executed that would be amazing!
(This is possible in powershell but I'm wondering if it's possible in cmd.exe with clink)