diff --git a/README.md b/README.md index ebbb919..5cf8318 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,30 @@ iOpener ======= -A Sublime Text 3 package to make locating and opening files much faster, by -opening from path instead of using the gui interface. Behaviour mostly emulates -emacs' find-file. With auto-completion, directory listings and history. +A Sublime Text 2/3 package to make finding and opening files less painful. +Keep your hands on the keyboard with smart auto completion, history, and directory listings. -Use ---- -Tap `super+o` or `cntl-o` as usual (or use the command pallete, and select the -command `Find File`). You will be greeted with an input panel that allows you to -type in file paths. +Usage +----- +Use `cmd + o` or `cntl + o` as usual (or choose `Find File` in the command pallete.) -- Pressing tab will cause completion to occur. +- Pressing tab will cause smart completion to occur (hint: `f1` can complete to `filename1`!) -- Double-tapping tab will allow you to search a current directory. +- Double-pressing tab will give you the directory listing -- Up/down allow you to navigate history. +- Up/down allow you to navigate previously entered paths -- Attempting to open a path that doesn't exist will give a message asking - whether or not to create the path. +- Opening a path that doesn't exist will create a new file or directory ![demo](https://raw.github.com/rosshemsley/iOpener/screenshots/demo.gif) -Note ----- -- This plugin will take over your default 'open file' shortcut. If you want to - use your system file-open dialog, use `super+shift+o` or `ctrl+shift+o` - depending on your OS. +NB +-- +- This plugin will take over your default 'open file' shortcut. To + use your system default, use `cmd + shift + o` or `ctrl + shift + o` - The open panel will not show unless a window is open. You will need to use - `cntl+n` or `super+n` to open a new window first. + `cntl + n` or `cmd + n` to open a new window first Installing @@ -37,10 +32,6 @@ Installing Installation is easiest using Package Control. In the Command Pallette, select "Install Package" and then select "iOpener". -Credits -------- -I looked at lots of other people's code to write this, including in particular -SublimeMRU and FuzzyFileNav. Thanks for your inspiration! Bugs ---- diff --git a/matching.py b/matching.py index e7b7a06..ee5ba4a 100644 --- a/matching.py +++ b/matching.py @@ -11,6 +11,11 @@ class COMPLETION_TYPE: def complete_path(filename, directory_listing, case_sensitive=False): + lcs_completion = get_lcs_completion_or_none(filename, directory_listing) + + if lcs_completion is not None: + return lcs_completion, COMPLETION_TYPE.Complete + matches = get_matches(filename, directory_listing, case_sensitive) if len(matches) > 1: @@ -34,11 +39,91 @@ def longest_completion(filename, matches): return filename + commonprefix(matches)[len(filename):] +def get_lcs_completion_or_none(filename, directory_listing): + """ + If there is a unique way to complete the path such that the LCS is the same + as the query string, return that (similar to the way Fish shell works) + """ + completion = None + + for candidate in directory_listing: + if lcs(filename, candidate) == filename: + if completion is not None: + return None + else: + completion = candidate + + return completion + + +def lcs(A, B): + """ + Taken and adapted from + http://rosettacode.org/wiki/Longest_common_subsequence#Dynamic_Programming_7 + """ + lengths = [[0 for j in range(len(B) + 1)] for i in range(len(A) + 1)] + + for i, x in enumerate(A): + for j, y in enumerate(B): + if x == y: + lengths[i + 1][j + 1] = lengths[i][j] + 1 + else: + lengths[i + 1][j + 1] = max(lengths[i + 1][j], lengths[i][j + 1]) + + result = "" + x, y = len(A), len(B) + while x != 0 and y != 0: + if lengths[x][y] == lengths[x-1][y]: + x -= 1 + elif lengths[x][y] == lengths[x][y-1]: + y -= 1 + else: + result = A[x-1] + result + x -= 1 + y -= 1 + return result + + ## # Unit tests ## +class TestLCSCompletion(TestCase): + def test1(self): + directory_listing = [ + 'filename1', + 'filename2', + 'test', + ] + + output1 = get_lcs_completion_or_none('file1', directory_listing) + self.assertEqual('filename1', output1) + + output2 = get_lcs_completion_or_none('file2', directory_listing) + self.assertEqual('filename2', output2) + + output3 = get_lcs_completion_or_none('tst', directory_listing) + self.assertEqual('test', output3) + + output4 = get_lcs_completion_or_none('tes', directory_listing) + self.assertEqual('test', output4) + + output5 = get_lcs_completion_or_none('django', directory_listing) + self.assertIsNone(output5) + + +class TestLCS(TestCase): + def test1(self): + self.assertEqual('hel', lcs('hello', 'heal')) + self.assertEqual('oe', lcs('hope', 'oe')) + self.assertEqual('', lcs('this', 'xyz')) + self.assertEqual('bcd', lcs('abcdefg', 'bcd')) + self.assertEqual('test', lcs('test', 'test')) + self.assertEqual('rd', lcs('read', 'rd')) + self.assertEqual('round', lcs('round', 'arounded')) + + class TestCompletion(TestCase): def test1(self): filename = 'test'