Sublime Forum

ST3 style autocomplete in ST4

#13

It would require ST to collect all completions from both plugin_hosts, merge them with its own candidates (word completions, sublime-completions) and than push results back to plugin_hosts

All this besides the final push is already happening. And we don’t need that push or pull until some plugin asks for a list. I.e. I can create a python command, assign it to be executed on Tab and there I call sublime.get_auto_complete_options(n=5) or pass a primer explicitly sublime.get_auto_complete_options("prp", n=5) when do whatever I want with it.

Since I am doing that intentionally I am prepared to pay whatever performance cost to be there. I don’t expect that to be really slow though.

0 Likes

#14

I also don’t like the new visual auto complete or mini auto complete thing as I always used insert best completion on a simple <TAB>. And only sometimes ctrl+space for the auto complete popup.

Actually, having auto complete popups in the other IDE’s made me switch to SublimeText in the first place as a distraction free editor helps me to keep my focus.

Now using Sublime Text for years, I learned an abbreviation style
writing which works throughout Sublime, for example in Goto Symbol/Reference, Goto File.

For example, I now, really rel-learning how I type in Sublime, usually write fbs<TAB> to get for example find_by_selector. I write limo<TAB>.PE<TAB> which expands to linter_module.PermanentError. Now the exact expansion on
first tab is sometimes magic, sometimes not helpful, but it is suprisingly good and predictable in the context. (It’s like an automatic, learning snippet system.) It usually works, and I often know in the context of what I’m writing that I need a limo<tab> because lm<tab> will expand to something different.

Basically, I use “auto complete” on demand when I don’t know exactly what I’m looking for. But just “insert best completion” on abbreviations when I’m in the zone, how they call it.

How Sublime handles and remembers used abbreviations is a first class feature of Sublime. Think of how you program the “Command Palette” so that chm maybe selects checkout master whereby chb selects checkout new branch.

3 Likes

#15
import sublime
import sublime_plugin
import re


matching = []
last_choice = ''
lookup_index = 0
class HippieWordCompletionCommand(sublime_plugin.TextCommand):
	def run(self, edit):
		global last_choice
		global lookup_index
		global matching
		search_word_region = self.view.word(self.view.sel()[0])
		search_word_text = self.view.substr(search_word_region)

		if search_word_text != last_choice:
			lookup_index = 0
			matching = []

			search_word_parts = re.findall('([A-Z])?([^A-Z]*)', search_word_text)
			search_word_parts = [item for t in search_word_parts for item in t if item]
			# print(search_word_parts)

			# [matching.append(s) for s in reversed(word_list) if s not in matching and search_word_text in s and s != search_word_text]
			for word in reversed(word_list):
				found = False
				if word not in matching and word != search_word_text and word[0] == search_word_text[0]:
					for word_part in  search_word_parts:
						if word_part in word:
							found = True
						else:
							found = False
							break
				if found:
					matching.append(word)

			if not matching:
				for w_list in word_list_global.values():
					[matching.append(s) for s in w_list if s not in matching and search_word_text in s and s != search_word_text]

			if not matching:
				return

		else:
			lookup_index += 1

			
		try:
			last_choice = matching[lookup_index]
		except IndexError:
			lookup_index = 0
		finally:
			last_choice = matching[lookup_index]

		for caret in self.view.sel():
			self.view.replace(edit, self.view.word(caret), last_choice)

		# self.view.replace(edit, search_word_region, last_choice)


word_list_global = {}
word_pattern = re.compile(r'(\w+)', re.S)
class TextChange(sublime_plugin.EventListener):
	def on_init(self, views):
		global word_list_global
		# [print(a.file_name()) for a in views]
		for view in views:
			contents = view.substr(sublime.Region(0, view.size()))
			word_list_global[view.file_name()] = word_pattern.findall(contents)

		# print(word_list_global)
		

	def on_modified_async(self, view):
		global word_list
		try:
			first_half  = view.substr(sublime.Region(0, view.sel()[0].begin()))
			second_half = view.substr(sublime.Region(view.sel()[0].begin(), view.size()))
			word_list = word_pattern.findall(second_half)
			word_list.extend(word_pattern.findall(first_half))
			word_list_global[view.file_name()] = word_list
			# print(word_list)
		except:
			pass


// Keymap


	{ "keys": ["tab"], "command": "hippie_word_completion", "context":
	[
		{ "key": "has_snippet", "operand": false  },
		{ "key": "preceding_text", "operator": "regex_contains", "operand": "\\w+", "match_all": true },
	]},

I wrote this crappy little plugin which basically bring backs the old completion method, there’s room for improvement obviously as I don’t code in python very much.

2 Likes

#16

Thanks for creating an example. I suspect it won’t be really efficient for bigger files as it reparses on each change. One might use TextChangeListener instead.

The bigger issue any other things supplying completions like Anaconda or new Sublime Text 4 index won’t be used here. Maybe using a separate key binding for those and Tab for fast completion like this would be enough though.

0 Likes

#17

I’m coming from CLion IDE and Tab Completion works just like this, it’s call “Hippie Word Completion” or “Cyclic Expand Word”, it also works similarly on Emacs, for more advance completion you press Ctrl+Space on CLion. I’m planning on using this plugin. Can you please give me an example on how to use “TextChangeListener” ? So I can incorporate it in my code. Thanks.

0 Likes

#18

For more advanced completion you can extend LSP package,
here’s an example


import weakref
from .core.protocol import Request, SymbolInformation, SymbolTag
from .core.registry import LspTextCommand
from .core.typing import Any, List, Tuple, Dict, Union
from .core.views import SYMBOL_KINDS
import os
import sublime


SUPPRESS_INPUT_SETTING_KEY = 'lsp_suppress_input'


def unpack_lsp_kind(kind: int) -> Tuple[int, str, str, str]:
    if 1 <= kind <= len(SYMBOL_KINDS):
        return SYMBOL_KINDS[kind - 1]
    return sublime.KIND_ID_AMBIGUOUS, "?", "???", "comment"


def format_symbol_kind(kind: int) -> str:
    if 1 <= kind <= len(SYMBOL_KINDS):
        return SYMBOL_KINDS[kind - 1][2]
    return str(kind)


def get_symbol_scope_from_lsp_kind(kind: int) -> str:
    if 1 <= kind <= len(SYMBOL_KINDS):
        return SYMBOL_KINDS[kind - 1][3]
    return 'comment'


def symbol_information_to_name(
    item: SymbolInformation,
    show_file_name: bool = True
) -> str:
    st_kind, st_icon, st_display_type, _ = unpack_lsp_kind(item['kind'])
    tags = item.get("tags") or []
    if SymbolTag.Deprecated in tags:
        st_display_type = "⚠ {} - Deprecated".format(st_display_type)
    container = item.get("containerName") or ""
    details = []  # List[str]
    if container:
        details.append(container)
    if show_file_name:
        file_name = os.path.basename(item['location']['uri'])
        details.append(file_name)
    return item["name"]



class LspWorkspaceSymbolsTwoCommand(LspTextCommand):

    capability = 'workspaceSymbolProvider'

    def run(self, edit: sublime.Edit, symbol_query_input: str) -> None:
        if symbol_query_input:
            session = self.best_session(self.capability)
            if session:
                params = {"query": symbol_query_input}
                request = Request("workspace/symbol", params, None, progress=True)
                self.weaksession = weakref.ref(session)
                session.send_request(request, lambda r: self._handle_response(
                    symbol_query_input, r), self._handle_error)

    def _open_file(self, symbols: List[SymbolInformation], index: int) -> None:
        if index != -1:
            session = self.weaksession()
            if session:
                session.open_location_async(symbols[index]['location'], sublime.ENCODED_POSITION)

    def _handle_response(self, query: str, response: Union[List[SymbolInformation], None]) -> None:
        if response:
            matches = response
            print(list(map(symbol_information_to_name, matches)))
            # window = self.view.window()
            # if window:
            #     window.show_quick_panel(
            #         list(map(symbol_information_to_quick_panel_item, matches)),
            #         lambda i: self._open_file(matches, i))
        else:
            sublime.message_dialog("No matches found for query string: '{}'".format(query))

    def _handle_error(self, error: Dict[str, Any]) -> None:
        reason = error.get("message", "none provided by server :(")
        msg = "command 'workspace/symbol' failed. Reason: {}".format(reason)
        sublime.error_message(msg)

run this in command palate to test it out

view.run_command('lsp_workspace_symbols_two', {"symbol_query_input": "Tex"})
1 Like

Single keypress tab completion not allowed anymore in sublime text 4
#19

Refined the initial example by LightsOut8008: https://github.com/Suor/sublime-hippie-autocomplete.

Uses fuzzy search now, protect from huge views. Many things to be desired still but usable.

P.S. Renamed the repo and updated link here.

2 Likes

#20

Nice,
Btw I have already knocked out couple of TODOs in my code

  1. For multiple cursors:
		for caret in self.view.sel():
			self.view.replace(edit, self.view.word(caret), last_choice)
  1. Matching first letter
if word not in matching and word != search_word_text and word[0] == search_word_text[0]:

// word[0] == search_word_text[0] // this matches first letter

search_word_parts = re.findall('([A-Z])?([^A-Z]*)', search_word_text)
search_word_parts = [item for t in search_word_parts for item in t if item]

// above code separates searched word into lower and uppercase
so if you want search for : MyAwesomeVariable -  you type MAV then tab 
  1. prefer words closer to cursor?
    This splits the list in two halves (one from cursor to beginning of file and second from cursor to end of the file) and joins them end to end, so words starting from above the cursor will be at beginning of the list and words below cursor would be in reverse at the end of the list. You have the preference to cycle from beginning or the end.
first_half  = view.substr(sublime.Region(0, view.sel()[0].begin()))
second_half = view.substr(sublime.Region(view.sel()[0].begin(), view.size()))
word_list = word_pattern.findall(second_half)
word_list.extend(word_pattern.findall(first_half))

I’ll also update previously pasted code above.
Thanks.

0 Likes

#21
  1. What if primers are different?
  2. Didn’t work for me. I don’t see how it should, say I am trying to expand wl into word_list:
    • regex finds single word part instead of 2 – w and l
    • even if regex did it properly keys are looked in the word in any place not at the word starts
    • doesn’t try to work for global word list
  3. I saw that, but once I use fuzzy matching it’s either it’s scoring or preserving order. I chose fuzzy scoring so far.
0 Likes

#22
  1. I’m not familiar with primers in your code, so can’t answer that.

  2. That works for Capitalized words camelCase PascalCase etc without much effort. if you want support lower_case, need to do little more work on ‘candidate word’ after matching first letter against searched word.

  3. That’s up to preference I guess.

0 Likes

#23

great, just starred it on github and added it to the Sublime Text trhough git with Sublime Merge.

Well it is your code. Feel free to open a github account and sync your lastest changes there.

Thanks for the plugin, guys!

0 Likes

auto complete problem
#24
search_word_parts = re.findall('([A-Z])?([^A-Z]*)', search_word_text)
search_word_parts = [item for t in search_word_parts for item in t if item]
# print(search_word_parts)

# [matching.append(s) for s in reversed(word_list) if s not in matching and search_word_text in s and s != search_word_text]
for word in reversed(word_list):
    found = False
    if word not in matching and word != search_word_text and word[0] == search_word_text[0]:
        if len(search_word_text) > 1 and '_' in word: \\ this if clause is for supporting 'snake_case' words
            for char in search_word_text:
                if char in word:
                    found = True
                else:
                    found = False
                    break
        else:
            for word_part in  search_word_parts:
                if word_part in word:
                    found = True
                else:
                    found = False
                    break
    if found:
        matching.append(word)

added if clause to support ‘snake_case’ words now - wl ->Tab should expand to word_list

0 Likes

#25

I guess we are trying to achieve different things. I want wl to reliably expand into word_list first and then to bowl and then to wall_2. Your code will order them the same as the list.

BTW, now words that go just before the cursor will be last in your candidates list. You might want to reverse those.

0 Likes

#26

Well my preference is to expands to words above and closest to my cursor first, I’m not sure how you have ordered your list, if you use previously mention method to order the list, you can have preference to either expand to words closest and above to the cursor or closest and below the cursor.

0 Likes

#27

I currently save words into set, do not preserve order. Order them by shortest match span.

0 Likes

#28

I have created a fork https://github.com/litezzzout/sublime-hippie-words,
-added cycle back ability.
-added ability to add candidate words from other files once finished cycling through current file.

2 Likes

#29

I added ability to prioritize matching first letters in combined_words
I personaly don’t care for it but, realized other people may find it useful since everyone doesn’t use camelCase or PascalCase. Later I may add setting to enable/disable it.

0 Likes

#30

I added expanding abbreviations support for snake_case, CameCase and mixedCase too. Adjusted how scoring works:

wl -> word_list, word_list_index, wall
wli -> word_list_index, word_list
wol -> word_list, wollie, world

Also added simple history of chosen completions they will be selected first.

BTW, there was a bug in key bindings - indent almost never worked. Fixed it here, you might want to do the same.

0 Likes

#31

I mean if words under cursors differ then it will replace all of them with a suggestion for the first one. So current behavior is incorrect. It is still useful though, so I went for the same implementation as yours for now, might fix it later.

0 Likes

#32

I personally didn’t face any problem indenting a single line rather I couldn’t indent multiple lines with selection, so I fixed that.

On SublimeText3 it inserts 4 spaces or [tab] instead of completion, I personally don’t find it more useful that what we already have, but of course we can change it later.

0 Likes