-
Notifications
You must be signed in to change notification settings - Fork 7
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
Suffix Alias Script #13
Comments
Windows has "File Associations". That's how e.g. 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. |
Thank you! 😃 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 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 also how about adding this to the repository? |
There are some problems with that:
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
This isn't ready to be included in a repo:
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:
Warning The example above is not trying to fix the issue where |
Also, what about an input line You can use clink.parseline(text) to get a table of line_state objects for each of the commands in the |
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 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 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:
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
However with the new approach, where I add the 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 (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) |
Ah, ok, that makes it simpler and more consistent. This is sounding promising...
The zsh parity approach sounds interesting, and I'll load the script and share my observations after I've examined it a bit.
How does one specify them in zsh?
I agree that using multiple settings wouldn't result in a nice user experience. Trying to use multiple
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 If you want to debug it, you could run
You could use In the Lua debugger, you could use
With the additional info and simplification to match how zsh works, it's something I might be willing to add into clink-gizmos.
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.
The bugIt has nothing to do with the table structure. If you restore the commented-out code, then it will also not work. Because the code tries to construct a regular expression string instead of a Lua pattern string. PerformanceBut 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, optimizations can come laterThe 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 |
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 For example we can have a file,
And then when I make it executable and run it by typing its name, I'll have the 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 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. It seems to work for me, but I haven't tested it extensively. Note I added a 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! |
Just chiming in that Would it be hard to modify this to serve that purpose as well? Possibly toggling between behaviors with a clink setting? |
Kind of? It would be possible to create something similar to fish's abbr. But I wouldn't recommend using this script as a starting point for that -- there's very little similarity at an implementation level. A script could use clink.onaftercommand() and look for available expansions for text preceding the cursor position. But it would have to also check rl.getlastcommand() to test what command was used -- for example, a cursor navigation command like I don't plan to work on something like this. Also, I would personally not use something that automatically replaces what I type -- too often that kind of thing makes it impossible to type certain things. I think a key binding to perform expansion on demand would fit my own usage style better. But I don't plan to work on something like that, either. (And FWIW, I can't use the suffix script because it makes it impossible to run certain kinds of commands, because it unconditionally expands things -- even the .git example for |
Yeah, I understand. Filtering real-time vs one-shot would have different priorities. On my usage of fish abbreviations: For example, abbreviating So I've never faced inconveniences with abbreviations - but maybe that is because of the abbreviations I set. |
If in fish it only does something in response to Space then simply do that same thing in Clink.
See Lua key bindings. The suffix alias script is unrelated. |
Windows has Enabling Showing Input Hints in Clink lets doskey_hinter.lua show a preview of how an alias will expand. @plutonium-239 I've written a prototype script |
I knew about the alias hints - I have it enabled, but usually the hint pops up ~0.75s after typing (is this an artificial delay because of debouncing the input?) so I don't wait for it. Nice prototype! I liked that you added 2 undogroups as well. It works with the semi-minimal testing I've done, just needs some color. |
See comment_row.hint_delay. Default is 500 ms. |
Is there a reason for this delay? I couldn't find it in the doc |
Several reasons. Try it without a delay, they'll probably become clear. 🙃 |
I don't notice any lag/flashing/weird behaviour 😅 Maybe my list of macros is very small: la=ls -A $*
ll=ls -lAh $*
gitbash="C:\Program Files\Git\usr\bin\bash.exe" $*
gnutar="C:\Program Files\Git\usr\bin\tar.exe" $*
smerge="C:\Program Files\Sublime Merge"\smerge.exe $* What was supposed to happen? |
I imagine you are probably only looking at what happens with the When hints show up instantly, then there's flicker while typing (hints appear for a fraction of a second and disappear again), and also it shifts the input line up a row if at the bottom of the screen. And a couple other things that I forget offhand. I originally had no delay, and quickly realized that was not acceptable at all, in the big picture. |
These are Weirdly, I don't see any flicker, at all - I've even recorded it here I could even screen-record if you want, but this should be more accurate as it captures everything sent to the terminal. |
I think the reason you don't experience it as "flicker" is because you're typing very slowly. It's impossible to have flicker when typing slowly. Sorry for not explaining that; I assumed it was clear. |
Oh, understood, you mean to say the flicker caused by actually continuing typing. That doesn't bother me too much since I know it is because of my own actions and not something undesirable, and also serves as another way to check in passing if I entered the right thing. |
As you like. One kind of situation where flicker got really annoying was like this:
As you type |
Yeah that makes sense |
The abbr discussion was off-topic from this issue; I wish I had moved it to a separate issue for proper tracking. In any case, the abbr.lua script has been added to the clink-gizmos repo. |
Back on topic... The suffix alias script is still not added to clink-gizmos. Because even the original "git clone" example causes problems for me: when a suffix alias is defined for ".git" then it becomes impossible to run a command like "dir .git" because it gets replaced. This completely blocked me from doing multiple different kinds of operations when a suffix alias was defined. Suffix aliases seem problematic to me, so I haven't invested time finishing the TODO items in the prototype script. |
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)
The text was updated successfully, but these errors were encountered: