Sublime Forum

ST3 style autocomplete in ST4

#1

I used to autocomplete with a single Tab hit in ST3 and then hit again if don’t like an option. Compared to that in ST4:

  • I need to hit Tab twice for most likely option
  • I can hit twice repeatedly to get a new option, but that is not the same since next option are based on already updated text unlike in ST3
  • I see popup, which is annoying

I tried disabling popup by setting "auto_complete_delay": 50000000000,, but then does nothing.

Is there a way to get ST3 behavior back? It feels way swifter way for me.
Also is there a way to get rid of the popup?

P.S. "auto_complete": false removed popup on typing, I still see it on Tab

2 Likes

#2

P.S. "auto_complete": false removed popup on typing, I still see it on Tab

"auto_complete_commit_on_tab": false, would do the trick even though auto_complete: false should have already disabled it.

That said, old style insert_best_completions was dropped. It’s no longer available.

You could try mini_auto_complete: true. It shows a preview of what’s commited as phantom. It’s still undocumented feature due to some edge cases.

0 Likes

#3

"auto_complete_commit_on_tab": false only disables using Enter to select an item from completion list, otherwise noop.

mini_auto_complete: true sometimes uses a ghost, sometimes not. It does suppress autocomplete popup in text or typescript files but not in python ones. There it briefly appears and hides when popup appears.

It looks like Sublime Text moves into IDE territory, which I don’t understand really. It is too far of a way to PyCharm and friends, so it’s infeasible to catch up. Loosing qualities of less distraction text editor is far easier to do.

1 Like

#4

Is there some API to revive insert_best_completion with some python code?

0 Likes

#5

In what way is a small difference in how the auto-complete behaves “IDE territory”?

0 Likes

#6

Popups are IDE territory. Slow auto-completion is too - and both needing to hit Tab twice or even worse needing to read options from screen before hitting Tab are slow.

Go to definition is also IDE feature, but this one is easy to ignore as it doesn’t add to distraction

2 Likes

#7

@deathaxe is it a big deal to support insert_best_completion or at least an API call to list best completions? It looks like all the infrastructure is there anyway.

1 Like

#8

Sorry, it is "tab_completion", false instead.

That’s a bug, then. It shouldn’t show completion panel. As I said, there are still some edge cases remaining.

Auto completion was always present. On the other hand most feature requests target exactly this - IDE like features.

I personally found this tab cycling thing nasty and annoying as it often came into the way when trying to add tabs after words, but that’s purly personal taste for sure.

A plugin doesn’t have access to all completions, so yes, it’s not really possible.

0 Likes

#9

Also see discussion about it at https://github.com/sublimehq/sublime_text/issues/4174

0 Likes

#10

I like the style of mini auto complete, cycling were useful occasionally too though. Tab cycling was never an issue for me since I used spaces. If I would use tabs though I would be very annoyed too :slightly_smiling_face: and would have probably remapped that to some other keybinding.

Still it looks like exposing a list of completions to python shouldn’t be that much of an effort. Then any style of unobtrusive completion might be implemented on top in python.

0 Likes

#11

@deathaxe that was the question. It looks like exposing such call is a trivial matter, why not to?

0 Likes

#12

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 so they could decide what to do: Display, commit, … . In the meanwhile … what should happen? Who knows whether a plugin will do the job or not? And even if there was one, required/increased interprocess communication between sublime text and plugin hosts would probably cause performance issues.

The main auto-completion implementation and final logic to collect best matches is part of sublime text core. It just pulls candidates from plugins. Anything else would probably result in wonky behavior.

0 Likes

#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.

2 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

#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