├── .flake8 ├── .gitignore ├── .python-version ├── Context.sublime-menu ├── Default.sublime-commands ├── Default.sublime-keymap ├── LICENSE.txt ├── LaravelGoto.sublime-settings ├── Main.sublime-menu ├── lib ├── attribute.py ├── blade.py ├── classname.py ├── config.py ├── console.py ├── finder.py ├── inertia.py ├── language.py ├── livewire.py ├── logging.py ├── middleware.py ├── namespace.py ├── place.py ├── route_item.py ├── router.py ├── selection.py ├── setting.py └── workspace.py ├── main.py └── readme.md /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501 3 | exclude = .git,__pycache__,.github -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | package-metadata.json 6 | Default.sublime-mousemap -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "command": "laravel_goto" } 3 | ] 4 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Laravel Goto: Goto Implementation", 4 | "command": "laravel_goto" 5 | }, 6 | { 7 | "caption": "Laravel Goto: Go to Controller via Uris", 8 | "command": "goto_controller" 9 | }, 10 | { 11 | "caption": "Laravel Goto: Settings", 12 | "command": "edit_settings", 13 | "args": { 14 | "base_file": "${packages}/Laravel Goto/LaravelGoto.sublime-settings", 15 | "user_file": "${packages}/User/LaravelGoto.sublime-settings", 16 | "default": "// Settings in here override those in \"Laravel Goto/LaravelGoto.sublime-settings\"\n\n{\n\t$0\n}\n", 17 | }, 18 | }, 19 | { 20 | "caption": "Laravel Goto: Key Bindings", 21 | "command": "edit_settings", 22 | "args": { 23 | "base_file": "${packages}/Laravel Goto/Default.sublime-keymap", 24 | "user_file": "${packages}/User/Default (${platform}).sublime-keymap", 25 | "default": "[\n\t$0\n]\n", 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+;"], "command": "laravel_goto" } 3 | ] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adrian Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LaravelGoto.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // If the text ends with the following extensions, go to the path directly. 3 | "default_static_extensions": [ 4 | "js", 5 | "ts", 6 | "jsx", 7 | "vue", 8 | 9 | "css", 10 | "scss", 11 | "sass", 12 | "less", 13 | "styl", 14 | 15 | "htm", 16 | "html", 17 | "xhtml", 18 | "xml", 19 | 20 | "log" 21 | ], 22 | // Show hover popup if available 23 | "show_hover": true, 24 | // The location of PHP execute command 25 | "php_bin": "php" 26 | } -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "caption": "Package Settings", 9 | "mnemonic": "P", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "Laravel Goto", 14 | "children": [ 15 | { 16 | "caption": "Settings", 17 | "command": "edit_settings", 18 | "args": { 19 | "base_file": "${packages}/Laravel Goto/LaravelGoto.sublime-settings", 20 | "user_file": "${packages}/User/LaravelGoto.sublime-settings", 21 | "default": "// Settings in here override those in \"Laravel Goto/LaravelGoto.sublime-settings\"\n{\n\t$0\n}\n" 22 | } 23 | }, 24 | { 25 | "caption": "-" 26 | }, 27 | { 28 | "caption": "Key Bindings", 29 | "command": "edit_settings", 30 | "args": { 31 | "base_file": "${packages}/Laravel Goto/Default.sublime-keymap", 32 | "user_file": "${packages}/User/Default (${platform}).sublime-keymap", 33 | "default": "[\n\t$0\n]\n", 34 | }, 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | ] -------------------------------------------------------------------------------- /lib/attribute.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class Attribute: 6 | patterns = [ 7 | compile(r"""#\[([^(]+)\('([^"']+)"""), 8 | ] 9 | 10 | location_pattern = """['"]%s['"]\\s*=>""" 11 | 12 | files = { 13 | 'Auth': 'config/auth.php', 14 | 'Cache': 'config/cache.php', 15 | 'DB': 'config/database.php', 16 | 'Log': 'config/logging.php', 17 | 'Storage': 'config/filesystems.php', 18 | } 19 | 20 | def get_place(self, path, line, lines=''): 21 | for pattern in self.patterns: 22 | matched = pattern.search(line) or pattern.search(lines) 23 | if matched is None: 24 | continue 25 | 26 | groups = matched.groups() 27 | if path != groups[1]: 28 | continue 29 | 30 | # Config file 31 | if 'Config' == groups[0]: 32 | split = path.split('.') 33 | path = 'config/' + split[0] + '.php' 34 | location = None 35 | if (2 <= len(split)): 36 | location = self.location_pattern % (split[1]) 37 | return Place(path, location) 38 | 39 | if groups[0] in self.files: 40 | path = self.files.get(groups[0]) 41 | location = self.location_pattern % (groups[1]) 42 | return Place(path, location) 43 | 44 | return False 45 | -------------------------------------------------------------------------------- /lib/blade.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class Blade: 6 | blade_patterns = [ 7 | compile(r"""\b(?:view|markdown)\b\(\s*(['"])([^'"]*)\1"""), 8 | compile(r"""\$view\s*=\s*(['"])([^'"]*)\1"""), 9 | compile(r"""\b(?:view|text|html|markdown)\b\s*:\s*(['"])([^'"]*)\1"""), 10 | compile(r"""view\(\s*['"][^'"]*['"],\s*(['"])([^'"]*)\1"""), 11 | compile(r"""[lL]ayout\(\s*(['"])([^'"]*)\1"""), 12 | compile(r"""['"]layout['"]\s*=>\s*(['"])([^'"]*)\1"""), 13 | compile(r"""@include(If\b)?\(\s*(['"])([^'"]*)\2"""), 14 | compile(r"""@extends\(\s*(['"])([^'"]*)\1"""), 15 | compile(r"""@include(When|Unless\b)?\([^'"]+(['"])([^'"]+)"""), 16 | compile(r"""View::exists\(\s*(['"])([^'"]*)\1"""), 17 | compile(r"""View::composer\(\s*(['"])([^'"]*)\1"""), 18 | compile(r"""View::creator\(\s*(['"])([^'"]*)\1"""), 19 | compile(r"""(resources\/views[^\s'"-]+)"""), 20 | ] 21 | 22 | multi_views_patterns = [ 23 | compile( 24 | r"""@includeFirst\(\[(\s*['"][^'"]+['"]\s*[,]?\s*){2,}\]""" 25 | ), 26 | compile( 27 | r"""View::composer\(\[(\s*['"][^'"]+['"]\s*[,]?\s*){2,}\]""" 28 | ), 29 | compile(r"""view\(\[(\s*['"][^'"]+['"]\s*[,]?\s*){2,}\]"""), 30 | compile(r"""@each\(['"][^'"]+['"]\s*,[^,]+,[^,]+,[^)]+"""), 31 | compile(r"""View::first[^'"]*(['"])([^'"]*)\1"""), 32 | ] 33 | 34 | fragment_patterns = [ 35 | compile(r"""->fragment\(\s*['"]([^'"]+)"""), 36 | compile(r"""->fragmentIf\(\s*.*,\s*['"]([^'"]+)""") 37 | ] 38 | 39 | multi_fragments_patterns = [ 40 | compile(r"""->fragments\(\s*\[(\s*['"][^'"]+['"]\s*[,]?\s*){2,}\s*\]"""), 41 | compile(r"""->fragmentsIf\(\s*.*,\s*\[(\s*['"][^'"]+['"]\s*[,]?\s*){2,}\s*\]""") 42 | ] 43 | 44 | location_pattern = """fragment\\(\\s*['"]%s['"]\\s*\\)""" 45 | 46 | def get_place(self, path, line, lines=''): 47 | 48 | for pattern in self.blade_patterns: 49 | matched = pattern.search(line) or pattern.search(lines) 50 | if matched is None: 51 | continue 52 | 53 | groups = matched.groups() 54 | if path == groups[-1]: 55 | path = groups[-1].strip() 56 | path = self.transform_blade(path) 57 | return Place(path) 58 | 59 | for pattern in self.multi_views_patterns: 60 | if pattern.search(line) or pattern.search(lines): 61 | path = self.transform_blade(path) 62 | return Place(path) 63 | 64 | for frg_pattern in self.fragment_patterns: 65 | frg_matched = frg_pattern.search(lines) or frg_pattern.search(line) 66 | if frg_matched is None: 67 | continue 68 | 69 | for pattern in self.blade_patterns: 70 | matched = pattern.search(line) or pattern.search(lines) 71 | if matched is None: 72 | continue 73 | 74 | file = matched.groups()[-1].strip() 75 | file = self.transform_blade(file) 76 | location = self.location_pattern % path 77 | return Place(file, location) 78 | 79 | for frg_pattern in self.multi_fragments_patterns: 80 | frg_matched = frg_pattern.search(lines) or frg_pattern.search(line) 81 | if frg_matched is None: 82 | continue 83 | 84 | for pattern in self.blade_patterns: 85 | matched = pattern.search(line) or pattern.search(lines) 86 | if matched is None: 87 | continue 88 | 89 | file = matched.groups()[-1].strip() 90 | file = self.transform_blade(file) 91 | location = self.location_pattern % path 92 | return Place(file, location) 93 | 94 | return False 95 | 96 | def transform_blade(self, path): 97 | split = path.split(':') 98 | vendor = '' 99 | # vendor or namespace 100 | if (3 == len(split)): 101 | # vendor probably is lowercase 102 | if (split[0] == split[0].lower()): 103 | vendor = split[0] + '/' 104 | 105 | path = split[-1] 106 | path = vendor + path.replace('.', '/') 107 | if path.endswith('/blade/php'): 108 | path = path[:-1*len('/blade/php')] 109 | 110 | path += '.blade.php' 111 | return path 112 | -------------------------------------------------------------------------------- /lib/classname.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class ClassName: 6 | patterns = [ 7 | compile(r"""([A-Z][\w]+[/\\])+[A-Z][\w]+""") 8 | ] 9 | 10 | def get_place(self, path, line, lines=''): 11 | for pattern in self.patterns: 12 | 13 | matched = pattern.search(line) or pattern.search(lines) 14 | if matched: 15 | return Place(path + '.php') 16 | 17 | return False 18 | -------------------------------------------------------------------------------- /lib/config.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class Config: 6 | config_patterns = [ 7 | compile(r"""Config::[^'"]*(['"])([^'"]*)\1"""), 8 | compile(r"""config\([^'"]*(['"])([^'"]*)\1"""), 9 | ] 10 | 11 | find_pattern = """(['"]{1})%s\\1\\s*=>""" 12 | 13 | def get_place(self, path, line, lines=''): 14 | 15 | for pattern in self.config_patterns: 16 | matched = pattern.search(line) or pattern.search(lines) 17 | if matched is None: 18 | continue 19 | 20 | if not matched.group(2).startswith(path): 21 | continue 22 | 23 | split = path.split('.') 24 | path = 'config/' + split[0] + '.php' 25 | location = None 26 | if (2 <= len(split)): 27 | location = self.find_pattern % (split[1]) 28 | return Place(path, location) 29 | 30 | return False 31 | -------------------------------------------------------------------------------- /lib/console.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .place import Place 3 | from . import workspace 4 | import os 5 | 6 | 7 | class Console: 8 | def __init__(self, console_kernel=None): 9 | self.console_kernel = console_kernel 10 | if self.console_kernel: 11 | return 12 | for folder in workspace.get_folders(): 13 | fullpath = workspace.get_path(folder, 'app/Console/Kernel.php') 14 | if not fullpath: 15 | continue 16 | 17 | self.folder = os.path.dirname(fullpath) 18 | self.console_kernel = workspace.get_file_content(fullpath) 19 | if self.console_kernel: 20 | return 21 | 22 | def all(self): 23 | commands = {} 24 | if not self.console_kernel: 25 | return commands 26 | 27 | files = self.collect_files() 28 | commands = self.collect_file_cmds(files) 29 | commands.update(self.collect_registered_cmds()) 30 | return commands 31 | 32 | def get_command_signature(self, content): 33 | match = re.search(r"""\$signature\s*=\s*['"]([^\s'"]+)""", content) 34 | if match: 35 | return match.group(1) 36 | return '' 37 | 38 | def collect_files(self): 39 | ''' 40 | collect command files from $this->load(__DIR__) 41 | ''' 42 | files = [] 43 | match = re.search( 44 | r"""function commands\([^\)]*[^{]+([^}]+)""", 45 | self.console_kernel 46 | ) 47 | if not match: 48 | return files 49 | 50 | for match in re.findall( 51 | r"""\$this->load\(\s*__DIR__\s*\.\s*['"]([^'"]+)""", 52 | match.group(1) 53 | ): 54 | if match.startswith('/'): 55 | match = match[1:] 56 | 57 | folder = os.path.join(self.folder, match) 58 | files += workspace.get_recursion_files(folder) 59 | return files 60 | 61 | def collect_file_cmds(self, files): 62 | commands = {} 63 | for file in files: 64 | content = workspace.get_file_content(file) 65 | signature = self.get_command_signature(content) 66 | if signature: 67 | commands[signature] = Place(os.path.basename(file), uri=file) 68 | 69 | return commands 70 | 71 | def collect_registered_cmds(self): 72 | ''' 73 | collect commands from $command = [ 74 | 75 | ] 76 | ''' 77 | commands = {} 78 | match = re.search( 79 | r"""\$commands\s*=\s*\[([^\]]+)""", 80 | self.console_kernel, 81 | re.M 82 | ) 83 | if not match: 84 | return commands 85 | 86 | classes = match.group(1).splitlines() 87 | for class_name in classes: 88 | filename = workspace.class_2_file(class_name) 89 | 90 | if filename == '.php': 91 | continue 92 | 93 | for folder in workspace.get_folders(): 94 | uri = workspace.get_path(folder, filename, True) 95 | if not uri: 96 | continue 97 | content = workspace.get_file_content(uri) 98 | signature = self.get_command_signature(content) 99 | if signature: 100 | commands[signature] = Place(filename, uri=uri) 101 | 102 | return commands 103 | -------------------------------------------------------------------------------- /lib/finder.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .namespace import Namespace 3 | from .place import Place 4 | from .middleware import Middleware 5 | from .console import Console 6 | from .router import Router 7 | from .language import Language 8 | from .blade import Blade 9 | from .attribute import Attribute 10 | from .config import Config 11 | from .inertia import Inertia 12 | from .livewire import Livewire 13 | from .classname import ClassName 14 | from .setting import Setting 15 | 16 | 17 | def get_place(selection): 18 | line = selection.get_line() 19 | lines = selection.get_lines_after_delimiter() 20 | 21 | path = selection.get_path() 22 | 23 | places = ( 24 | path_helper_place, 25 | static_file_place, 26 | env_place, 27 | config_place, 28 | filesystem_place, 29 | lang_place, 30 | inertia_place, 31 | livewire_place, 32 | component_place, 33 | middleware_place, 34 | command_place, 35 | route_place, 36 | attribute_place, 37 | blade_place, 38 | controller_place, 39 | class_name_place, 40 | ) 41 | 42 | for fn in places: 43 | place = fn(path, line, lines, selection) 44 | if place: 45 | place.source = fn.__name__ 46 | return place 47 | 48 | 49 | def set_controller_action(path, action, blocks): 50 | ''' set the controller action ''' 51 | 52 | path = path.replace('@', '.php@') 53 | path = path.replace('::class', '.php') 54 | if action: 55 | path = path + '@' + action 56 | 57 | elif len(blocks) and blocks[0]['is_namespace'] is False: 58 | """resource or controller route""" 59 | new_path = blocks[0]['namespace'] 60 | if new_path != path: 61 | path = new_path + '.php@' + path 62 | else: 63 | path = new_path + '.php' 64 | 65 | return path 66 | 67 | 68 | def set_controller_namespace(path, selected, ns): 69 | ''' set the controller namespace ''' 70 | 71 | if '\\' != path[0] and ns: 72 | # it's not absolute path namespace, get group namespace 73 | path = ns + '\\' + path.lstrip('\\') 74 | 75 | return path 76 | 77 | 78 | def controller_place(path, line, lines, selected): 79 | namespace = Namespace(selected.view) 80 | blocks = namespace.get_blocks(selected) 81 | is_controller = "Controller" in lines or selected.is_class 82 | 83 | if is_controller is False and 0 == len(blocks): 84 | return False 85 | 86 | action = None 87 | pattern = compile(r"""\[\s*(.*::class)\s*,\s*["']([^"']+)""") 88 | matched = pattern.search(line) or pattern.search(lines) 89 | if (matched and path == matched.group(2)): 90 | path = matched.group(1) 91 | action = matched.group(2) 92 | 93 | path = set_controller_action(path, action, blocks) 94 | 95 | ns = namespace.find(blocks) 96 | path = set_controller_namespace(path, selected, ns) 97 | 98 | place = Place(path) 99 | place.is_controller = True 100 | return place 101 | 102 | 103 | def config_place(path, line, lines, selected): 104 | config = Config() 105 | place = config.get_place(path, line, lines) 106 | return place 107 | 108 | 109 | def filesystem_place(path, line, lines, selected): 110 | pattern = compile(r"""Storage::disk\(\s*['"]([^'"]+)""") 111 | matched = pattern.search(line) or pattern.search(lines) 112 | if (matched and path == matched.group(1)): 113 | path = 'config/filesystems.php' 114 | location = "(['\"]{1})" + matched.group(1) + "\\1\\s*=>" 115 | return Place(path, location) 116 | 117 | return False 118 | 119 | 120 | def inertia_place(path, line, lines, selected): 121 | inertia = Inertia() 122 | place = inertia.get_place(path, line, lines) 123 | return place 124 | 125 | 126 | def livewire_place(path, line, lines, selected): 127 | livewire = Livewire() 128 | place = livewire.get_place(path, line, lines) 129 | return place 130 | 131 | 132 | def lang_place(path, line, lines, selected): 133 | lang_patterns = [ 134 | compile(r"""__\([^'"]*(['"])([^'"]*)\1"""), 135 | compile(r"""@lang\([^'"]*(['"])([^'"]*)\1"""), 136 | compile(r"""trans\([^'"]*(['"])([^'"]*)\1"""), 137 | compile(r"""trans_choice\([^'"]*(['"])([^'"]*)\1"""), 138 | ] 139 | 140 | language = None 141 | for pattern in lang_patterns: 142 | matched = pattern.search(line) or pattern.search(lines) 143 | if (not matched or path != matched.group(2)): 144 | continue 145 | 146 | if not language: 147 | language = Language() 148 | place = language.get_place(path) 149 | return place 150 | 151 | return False 152 | 153 | 154 | def static_file_place(path, line, lines, selected): 155 | find = (path.split('.')[-1].lower() in Setting().exts()) 156 | if find is False: 157 | return False 158 | 159 | # remove dot symbols 160 | split = list(filter( 161 | lambda x: x != '..' and x != '.', 162 | path.split('/'))) 163 | return Place('/'.join(split)) 164 | 165 | 166 | def env_place(path, line, lines, selected): 167 | env_pattern = compile(r"""env\(\s*(['"])([^'"]*)\1""") 168 | matched = env_pattern.search(line) or env_pattern.search(lines) 169 | find = (matched and path == matched.group(2)) 170 | if find: 171 | return Place('.env', path) 172 | return False 173 | 174 | 175 | def component_place(path, line, lines, selected): 176 | component_pattern = compile(r"""<\/?x-([^\/\s>]*)""") 177 | matched = component_pattern.search(line) or component_pattern.search(lines) 178 | if matched is None: 179 | return False 180 | 181 | path = matched.group(1).strip() 182 | 183 | split = path.split(':') 184 | vendor = 'View/Components/' 185 | res_vendor = 'views/components/' 186 | # vendor or namespace 187 | if (3 == len(split)): 188 | # vendor probably is lowercase 189 | if (split[0] == split[0].lower()): 190 | vendor = split[0] + '/' 191 | res_vendor = split[0] + '/' 192 | 193 | sections = split[-1].split('.') 194 | place = Place(res_vendor + '/'.join(sections) + '.blade.php') 195 | place.paths.append(place.path) 196 | 197 | for i, s in enumerate(sections): 198 | sections[i] = s.capitalize() 199 | sections[-1] = camel_case(sections[-1]) 200 | place.paths.append(vendor + '/'.join(sections) + '.php') 201 | 202 | return place 203 | 204 | 205 | def camel_case(snake_str): 206 | components = snake_str.split('-') 207 | return components[0].title() + ''.join(x.title() for x in components[1:]) 208 | 209 | 210 | def attribute_place(path, line, lines, selected): 211 | attribute = Attribute() 212 | place = attribute.get_place(path, line, lines) 213 | return place 214 | 215 | 216 | def blade_place(path, line, lines, selected): 217 | blade = Blade() 218 | place = blade.get_place(path, line, lines) 219 | return place 220 | 221 | 222 | def path_helper_place(path, line, lines, selected): 223 | path_helper_pattern = compile(r"""([\w^_]+)_path\(\s*(['"])([^'"]*)\2""") 224 | matched = path_helper_pattern.search(line) or\ 225 | path_helper_pattern.search(lines) 226 | if (matched and path == matched.group(3)): 227 | prefix = matched.group(1) + '/' 228 | if 'base/' == prefix: 229 | prefix = '' 230 | elif 'resource/' == prefix: 231 | prefix = 'resources/' 232 | 233 | return Place(prefix + path) 234 | return False 235 | 236 | 237 | def middleware_place(path, line, lines, selected): 238 | middleware_patterns = [ 239 | compile(r"""[m|M]iddleware\(\s*\[?\s*(['"][^'"]+['"]\s*,?\s*)+"""), 240 | compile(r"""['"]middleware['"]\s*=>\s*\s*\[?\s*(['"][^'"]+['"]\s*,?\s*){1,}\]?"""), 241 | ] 242 | middlewares = None 243 | for pattern in middleware_patterns: 244 | matched = pattern.search(line) or pattern.search(lines) 245 | if not matched: 246 | continue 247 | 248 | if not middlewares: 249 | middleware = Middleware() 250 | middlewares = middleware.all() 251 | 252 | # remove middleware parameters 253 | alias = path.split(':')[0] 254 | place = middlewares.get(alias) 255 | if place: 256 | return place 257 | 258 | 259 | def command_place(path, line, lines, selected): 260 | patterns = [ 261 | compile(r"""Artisan::call\(\s*['"]([^\s'"]+)"""), 262 | compile(r"""command\(\s*['"]([^\s'"]+)"""), 263 | ] 264 | 265 | commands = None 266 | for pattern in patterns: 267 | match = pattern.search(line) or pattern.search(lines) 268 | if not match: 269 | continue 270 | 271 | if not commands: 272 | console = Console() 273 | commands = console.all() 274 | 275 | signature = match.group(1) 276 | place = commands.get(signature) 277 | if place: 278 | return place 279 | 280 | return place 281 | 282 | 283 | def route_place(path, line, lines, selected): 284 | patterns = [ 285 | compile(r"""route\(\s*['"]([^'"]+)"""), 286 | compile(r"""['"]route['"]\s*=>\s*(['"])([^'"]+)"""), 287 | ] 288 | 289 | routes = None 290 | for pattern in patterns: 291 | match = pattern.search(line) or pattern.search(lines) 292 | if not match: 293 | continue 294 | 295 | if not routes: 296 | router = Router() 297 | routes = router.all() 298 | 299 | place = routes.get(match.group(1)) 300 | if place: 301 | return place 302 | 303 | return place 304 | 305 | 306 | def class_name_place(path, line, lines, selected): 307 | class_name = ClassName() 308 | place = class_name.get_place(path, line, lines) 309 | return place 310 | -------------------------------------------------------------------------------- /lib/inertia.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class Inertia: 6 | 7 | inertia_patterns = [ 8 | compile(r"""Route::inertia\s*\([^,]+,\s*['"]([^'"]+)"""), 9 | compile(r"""Route::inertia\s*\([^,]+,\s*component\s*:\s*['"]([^'"]+)"""), 10 | compile(r"""Inertia::render\s*\(\s*['"]([^'"]+)"""), 11 | compile(r"""Inertia::render\s*\(\s*component\s*:\s*['"]([^'"]+)"""), 12 | compile(r"""inertia\s*\(\s*['"]([^'"]+)"""), 13 | compile(r"""inertia\s*\(\s*component\s*:\s*['"]([^'"]+)"""), 14 | ] 15 | 16 | def get_place(self, path, line, lines=''): 17 | 18 | for pattern in self.inertia_patterns: 19 | matched = pattern.search(line) or pattern.search(lines) 20 | if (matched and matched.group(1) in path): 21 | return Place(matched.group(1)) 22 | 23 | return False 24 | -------------------------------------------------------------------------------- /lib/language.py: -------------------------------------------------------------------------------- 1 | import os 2 | from . import workspace 3 | from .logging import info 4 | from .place import Place 5 | 6 | 7 | routes = {} 8 | 9 | 10 | class Language: 11 | find_pattern = """(['"]{1})%s\\1\\s*=>""" 12 | 13 | def __init__(self): 14 | self.base = None 15 | self.langs = {} 16 | 17 | for folder in workspace.get_folders(): 18 | dir = self.get_lang_dir(folder) 19 | if not dir: 20 | continue 21 | 22 | self.base = dir 23 | self.langs = {} 24 | 25 | with os.scandir(dir) as entries: 26 | dirs = [entry.name for entry in entries] 27 | for dir in dirs: 28 | if os.path.isdir(os.path.join(self.base, dir)): 29 | self.langs[dir] = True 30 | elif dir.endswith('.json'): 31 | self.langs[dir] = False 32 | info('lang base', self.base) 33 | info('langs', self.langs) 34 | return 35 | 36 | def get_lang_dir(self, base): 37 | dir = workspace.get_folder_path(base, 'resources/lang') 38 | if dir: 39 | return dir 40 | ''' For Laravel after 9.x ''' 41 | dir = workspace.get_folder_path(base, 'lang/en') 42 | if dir: 43 | return os.path.dirname(dir) 44 | return 45 | 46 | def get_place(self, path): 47 | split = path.split(':') 48 | vendor = '' 49 | # it's package trans 50 | if (3 == len(split)): 51 | vendor = 'vendor/' + split[0] + '/' 52 | keys = split[-1].split('.') 53 | path = f"lang/{vendor}{keys[0]}.php" 54 | 55 | uris = [] 56 | paths = [] 57 | locations = {} 58 | for lang, is_dir in self.langs.items(): 59 | lang_path = lang 60 | if is_dir: 61 | lang_path = f"{vendor}{lang}/{keys[0]}.php" 62 | else: 63 | jsonKey = '\\.'.join(keys) 64 | locations[lang] = jsonKey 65 | paths.append('lang/' + lang_path) 66 | 67 | uri = os.path.join(self.base, lang_path) 68 | if workspace.is_file(uri): 69 | uris.append(uri) 70 | 71 | location = None 72 | if (2 <= len(keys)): 73 | location = self.find_pattern % (keys[1]) 74 | 75 | place = Place(path, location) 76 | place.paths = paths 77 | place.paths.sort() 78 | place.uris = uris 79 | place.locations = locations 80 | 81 | return place 82 | -------------------------------------------------------------------------------- /lib/livewire.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from .place import Place 3 | 4 | 5 | class Livewire: 6 | 7 | patterns = [ 8 | compile(r"""livewire:([^\s"'>]+)"""), 9 | compile(r"""@livewire\s*\(\s*['"]([^'"]+)"""), 10 | 11 | ] 12 | 13 | def get_place(self, path, line, lines=''): 14 | 15 | for pattern in self.patterns: 16 | matched = pattern.search(line) or pattern.search(lines) 17 | if matched: 18 | path = self.camel_case(matched.group(1)) 19 | path = path.replace('.', '/') + '.php' 20 | return Place(path) 21 | 22 | return False 23 | 24 | def camel_case(self, snake_str): 25 | components = snake_str.split('-') 26 | return components[0].title() + ''.join(x.title() for x in components[1:]) 27 | -------------------------------------------------------------------------------- /lib/logging.py: -------------------------------------------------------------------------------- 1 | from .setting import Setting 2 | import logging 3 | 4 | 5 | def get_logger(): 6 | logger = logging.getLogger('LaravelGoto') 7 | logger.setLevel(logging.INFO) 8 | return logger 9 | 10 | 11 | def is_debug(): 12 | return Setting().get('debug') 13 | 14 | 15 | def info(caption, *args): 16 | if is_debug(): 17 | logger = get_logger() 18 | logger.info(f"{caption}: {args}") 19 | 20 | 21 | def error(caption, *args): 22 | if is_debug(): 23 | logger = get_logger() 24 | logger.error(f"{caption}: {args}") 25 | 26 | 27 | def warn(caption, *args): 28 | if is_debug(): 29 | logger = get_logger() 30 | logger.warning(f"{caption}: {args}") 31 | 32 | 33 | def exception(caption, ex: Exception): 34 | if is_debug(): 35 | logger = get_logger() 36 | logger.exception(caption) 37 | -------------------------------------------------------------------------------- /lib/middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .place import Place 3 | from . import workspace 4 | 5 | 6 | class Middleware: 7 | def __init__(self, http_kernel=None): 8 | self.http_kernel = http_kernel 9 | if self.http_kernel: 10 | return 11 | for folder in workspace.get_folders(): 12 | self.http_kernel = workspace.get_file_content( 13 | folder, 14 | 'app/Http/Kernel.php' 15 | ) 16 | if self.http_kernel: 17 | return 18 | 19 | def all(self): 20 | middlewares = {} 21 | if not self.http_kernel: 22 | return middlewares 23 | 24 | # Before Laravel 10, middlewareAliases was called routeMiddleware. 25 | # They work the exact same way. 26 | aliasPattern = r"""(\$\bmiddlewareAliases\b|\$\brouteMiddleware\b)\s*=\s*\[([^;]+)""" 27 | 28 | match = re.search(aliasPattern, self.http_kernel, re.M) 29 | if match is None: 30 | return middlewares 31 | 32 | classnames = self.collect_classnames(self.http_kernel) 33 | 34 | pattern = re.compile(r"""['"]([^'"]+)['"]\s*=>\s*([^,\]]+)""") 35 | for match in pattern.findall(match.group()): 36 | classname = match[1].replace('::class', '').strip() 37 | if classnames.get(classname): 38 | classname = classnames.get(classname) 39 | classname = workspace.class_2_file(classname) 40 | 41 | middlewares[match[0]] = Place(classname) 42 | 43 | return middlewares 44 | 45 | def collect_classnames(self, content): 46 | ''' 47 | collect class aliases 48 | ''' 49 | classnames = {} 50 | pattern = re.compile(r"use\s+([^\s]+)\s+as+\s+([^;]+)") 51 | for match in pattern.findall(content): 52 | classnames[match[1]] = match[0].strip() 53 | 54 | return classnames 55 | -------------------------------------------------------------------------------- /lib/namespace.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from re import compile 3 | 4 | patterns = [ 5 | (compile(r"""(controller)\s*\(\s*['"]?([^'")]+)"""), False), 6 | (compile(r"""resource\s*\(\s*['"][^'"]+['"]\s*,\s*(['"]?)([^,'"]+)"""), False), 7 | (compile(r"""namespace\s*\(\s*(['"])\s*([^'"]+)\1"""), True), 8 | (compile(r"""['"]namespace['"]\s*=>\s*(['"])([^'"]+)\1"""), True), 9 | ] 10 | 11 | 12 | class Namespace: 13 | def __init__(self, view): 14 | self.fullText = view.substr(sublime.Region(0, view.size())) 15 | self.length = len(self.fullText) 16 | 17 | def find(self, blocks): 18 | ''' find the namespace of the selection''' 19 | for block in blocks: 20 | if block['is_namespace']: 21 | return block['namespace'] 22 | return False 23 | 24 | def get_blocks(self, selection): 25 | '''get all closure blocks''' 26 | blocks = [] 27 | for pattern, isNamespace in patterns: 28 | for match in pattern.finditer(self.fullText): 29 | start = match.start() 30 | if selection.a < start: 31 | continue 32 | 33 | end = self.get_end_position(start) 34 | if selection.b > end: 35 | continue 36 | 37 | blocks.append({ 38 | 'is_namespace': isNamespace, 39 | 'namespace': match.group(2).strip().replace('::class', ''), 40 | 'range': sublime.Region(start, end) 41 | }) 42 | return blocks 43 | 44 | def get_end_position(self, start): 45 | '''get the end position from the start position''' 46 | result = [] 47 | while self.length > start: 48 | if '{' == self.fullText[start]: 49 | result.append(start) 50 | elif '}' == self.fullText[start]: 51 | if 0 != len(result): 52 | result.pop() 53 | if 0 == len(result): 54 | return start 55 | start = start + 1 56 | 57 | return start 58 | -------------------------------------------------------------------------------- /lib/place.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class Place: 5 | def __init__(self, path, location=None, uri=None): 6 | self.path = path 7 | self.location = location 8 | self.is_controller = False 9 | self.uri = uri 10 | self.paths = [] 11 | self.uris = [] 12 | self.locations = {} 13 | self.source = None 14 | 15 | def __str__(self): 16 | return json.dumps({ 17 | "source": self.source, 18 | "path": self.path, 19 | "location": self.location, 20 | "is_controller": self.is_controller, 21 | "uri": self.uri, 22 | "paths": self.paths, 23 | "uris": self.uris, 24 | "locations": self.locations 25 | }, sort_keys=True, indent=2) 26 | -------------------------------------------------------------------------------- /lib/route_item.py: -------------------------------------------------------------------------------- 1 | class RouteItem: 2 | 3 | def __init__(self, route, place): 4 | if 'GET|HEAD' == route['method']: 5 | route['method'] = 'GET' 6 | 7 | self.label = route['method'] + ' ' + route['uri'] 8 | self.detail = route['action'] 9 | self.place = place 10 | -------------------------------------------------------------------------------- /lib/router.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import json 4 | 5 | from .place import Place 6 | from . import workspace 7 | from .setting import Setting 8 | from .logging import info, exception 9 | from .route_item import RouteItem 10 | 11 | 12 | class Router: 13 | artisan = None 14 | dir = None 15 | 16 | named_routes = {} 17 | uri_routes = [] 18 | 19 | def __init__(self): 20 | for folder in workspace.get_folders(): 21 | self.artisan = workspace.get_path(folder, 'artisan') 22 | self.dir = workspace.get_folder_path(folder, 'routes') 23 | if self.dir: 24 | return 25 | 26 | def update(self, filepath=None): 27 | ''' 28 | update routes if routes folder's files were changed 29 | ''' 30 | info('artisan', self.artisan) 31 | info('routes folder', self.dir) 32 | if not self.artisan or not self.dir: 33 | return 34 | 35 | is_routes_changed = self.is_changed(filepath) 36 | info('routes changed', is_routes_changed) 37 | if not is_routes_changed: 38 | return 39 | workspace.set_unchanged(self.dir) 40 | 41 | php = Setting().get('php_bin') 42 | if not php: 43 | return 44 | 45 | args = [ 46 | php, 47 | self.artisan, 48 | 'route:list', 49 | '--json' 50 | ] 51 | 52 | try: 53 | output = subprocess.check_output( 54 | args, 55 | cwd='/', 56 | stderr=subprocess.STDOUT, 57 | shell=os.name == 'nt' 58 | ) 59 | 60 | except subprocess.CalledProcessError as e: 61 | exception('route:list failed', e) 62 | return 63 | except FileNotFoundError as e: 64 | exception('file not found', e) 65 | return 66 | 67 | output = output.decode('utf-8') 68 | try: 69 | route_rows = json.loads(output) 70 | 71 | except ValueError as e: 72 | exception('json.loads', e) 73 | return 74 | 75 | self.named_routes.clear() 76 | self.uri_routes.clear() 77 | 78 | for route in route_rows: 79 | if 'Closure' == route['action']: 80 | continue 81 | 82 | path = route['action'] 83 | action = '__invoke' 84 | if '@' in route['action']: 85 | path, action = route['action'].split('@') 86 | 87 | place = Place( 88 | workspace.class_2_file(path) + '@' + action, 89 | ) 90 | place.is_controller = True 91 | 92 | self.named_routes[route['name']] = place 93 | self.uri_routes.append(RouteItem(route, place)) 94 | 95 | return True 96 | 97 | def is_changed(self, filepath=None): 98 | return workspace.is_changed(self.dir, filepath) 99 | 100 | def all(self): 101 | self.update() 102 | return self.named_routes 103 | 104 | def uris(self): 105 | self.update() 106 | return self.uri_routes 107 | -------------------------------------------------------------------------------- /lib/selection.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from re import sub 3 | 4 | 5 | class Selection(sublime.Region): 6 | delimiters = """<("'[,)> """ 7 | 8 | def __init__(self, view, point=None): 9 | self.view = view 10 | self.region = view.sel()[0] 11 | if point: 12 | self.region = sublime.Region(point, point) 13 | self.line = view.line(self.region) 14 | 15 | scopes = view.scope_name(self.region.begin()) 16 | self.is_class = 'support.class.php' in scopes 17 | 18 | selected = self.get_selection() 19 | super(Selection, self).__init__(selected.begin(), selected.end(), -1) 20 | 21 | def substr(self): 22 | return self.view.substr(self) 23 | 24 | def substr_line(self): 25 | return self.view.substr(self.line) 26 | 27 | def get_selection(self): 28 | if self.region.begin() != self.region.end(): 29 | return self.region 30 | 31 | return self.get_selected_by_delimiters(self.delimiters) 32 | 33 | def get_selected_by_delimiters(self, start_delims, end_delims=None): 34 | start = self.region.begin() 35 | end = self.region.end() 36 | 37 | if (end_delims is None): 38 | end_delims = start_delims 39 | while start > self.line.a: 40 | if self.view.substr(start - 1) in start_delims: 41 | break 42 | start -= 1 43 | 44 | while end < self.line.b: 45 | if self.view.substr(end) in end_delims: 46 | break 47 | end += 1 48 | return sublime.Region(start, end) 49 | 50 | def get_line(self): 51 | return self.substr_line().strip() 52 | 53 | def get_lines_after_delimiter(self, delimiter='('): 54 | lines = [] 55 | line_number, _ = self.view.rowcol(self.line.a) 56 | while line_number >= 0: 57 | point = self.view.text_point(line_number, 0) 58 | line = self.view.full_line(point) 59 | text = self.view.substr(line).strip() 60 | lines.insert(0, text) 61 | if text.__contains__(delimiter) and not text.startswith('->'): 62 | return ''.join(lines) 63 | 64 | line_number = line_number - 1 65 | 66 | return '' 67 | 68 | def get_path(self): 69 | path = self.substr().strip(self.delimiters + ' ') 70 | # remove the rest of string after { 71 | path = sub('{.*', '', path) 72 | # remove the rest of string after $ 73 | path = sub('\\$.*', '', path) 74 | # remove dot at the end 75 | path = path.rstrip('.') 76 | 77 | return path 78 | -------------------------------------------------------------------------------- /lib/setting.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | settings = None 4 | extensions = None 5 | 6 | 7 | class Setting: 8 | 9 | def __init__(self): 10 | global settings, extensions 11 | if not settings: 12 | settings = sublime.load_settings('LaravelGoto.sublime-settings') 13 | 14 | extensions = settings.get('default_static_extensions') 15 | exts = settings.get('static_extensions') 16 | if exts: 17 | extensions += exts 18 | 19 | # make sure extensions are lower case 20 | extensions = list( 21 | map(lambda ext: ext.lower(), extensions)) 22 | 23 | def get(self, name): 24 | return settings.get(name) 25 | 26 | def exts(self): 27 | return extensions 28 | -------------------------------------------------------------------------------- /lib/workspace.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | import sublime 4 | 5 | mTimes = {} 6 | contents = {} 7 | changes = {} 8 | 9 | 10 | def is_file(base, filename=None): 11 | fullpath = base 12 | if filename: 13 | fullpath = os.path.join([base, filename]) 14 | return os.path.isfile(fullpath) 15 | 16 | 17 | def is_changed(folder_path, file_path=None): 18 | ''' 19 | is the folder's files were changed 20 | :param file_path only check the file in the folder 21 | ''' 22 | 23 | if file_path: 24 | if not file_path.startswith(folder_path): 25 | return False 26 | 27 | mTime = os.path.getmtime(file_path) 28 | return mTimes.get(file_path) != mTime 29 | 30 | if folder_path not in changes: 31 | return True 32 | with os.scandir(folder_path) as entries: 33 | files = [entry for entry in entries if entry.is_file() or entry.is_dir()] 34 | if changes[folder_path] != len(files): 35 | return True 36 | for entry in files: 37 | fullpath = entry.path 38 | mTime = os.path.getmtime(fullpath) 39 | if mTimes.get(fullpath) != mTime: 40 | return True 41 | 42 | return False 43 | 44 | 45 | def set_unchanged(folder_path): 46 | ''' 47 | set the folder's files is changed 48 | ''' 49 | 50 | with os.scandir(folder_path) as entries: 51 | files = [entry.name for entry in entries if entry.is_file() or entry.is_dir()] 52 | changes[folder_path] = len(files) 53 | 54 | for file in files: 55 | fullpath = os.path.join(folder_path, file) 56 | mTime = os.path.getmtime(fullpath) 57 | mTimes[fullpath] = mTime 58 | 59 | 60 | def get_file_content(base, file_path=None): 61 | fullpath = base 62 | if file_path: 63 | fullpath = get_path(base, file_path) 64 | if not fullpath: 65 | return 66 | if not os.path.isfile(fullpath): 67 | return 68 | 69 | mTime = os.path.getmtime(fullpath) 70 | # from cache 71 | if mTimes.get(fullpath) == mTime: 72 | return contents.get(fullpath) 73 | 74 | # from disk 75 | with open(fullpath, mode="r", encoding="utf-8") as f: 76 | content = f.read() 77 | mTimes[fullpath] = mTime 78 | contents[fullpath] = content 79 | return content 80 | 81 | 82 | def get_recursion_files(folder, ext='.php'): 83 | ''' 84 | get all files including sub-dirs with the extension 85 | ''' 86 | files = [] 87 | p = Path(folder) 88 | for file in p.rglob(f'*{ext}'): 89 | if file.is_file(): 90 | files.append(str(file)) 91 | return files 92 | 93 | 94 | def get_folder_path(base, folder_name, recursion=True): 95 | ''' 96 | get real path by folder name 97 | ''' 98 | 99 | star = None 100 | folders = folder_name.split('/') 101 | if '*' == folders[-1]: 102 | star = folders.pop() 103 | folder_path = '/'.join(folders) 104 | 105 | full_folder_path = os.path.join(base, folder_path) 106 | if os.path.isdir(full_folder_path): 107 | if not star: 108 | return full_folder_path 109 | 110 | folders = [] 111 | with os.scandir(full_folder_path) as entries: 112 | for entry in entries: 113 | if entry.is_dir(): 114 | folders.append(entry.path) 115 | 116 | return folders 117 | 118 | if not recursion: 119 | return 120 | 121 | with os.scandir(base) as entries: 122 | for entry in entries: 123 | if not entry.is_dir(): 124 | continue 125 | 126 | folder = entry.path 127 | fullpath = get_folder_path(folder, folder_name, False) 128 | if fullpath: 129 | return fullpath 130 | 131 | 132 | def get_path(base, file_path, recursion=True): 133 | ''' 134 | get real path by a part of file path 135 | ''' 136 | top_dir = None 137 | if '/' in file_path: 138 | top_dir = file_path.split('/')[0] 139 | 140 | with os.scandir(base) as entries: 141 | files = [entry.name for entry in entries] 142 | if not top_dir and file_path in files: 143 | fullpath = os.path.join(base, file_path) 144 | if os.path.isfile(fullpath): 145 | return fullpath 146 | return None 147 | 148 | for file in files: 149 | if os.path.isdir(base + '/' + file) is False: 150 | continue 151 | 152 | # if not the right dictionary, search the sub dictionaries 153 | if top_dir != file: 154 | if recursion: 155 | fullpath = get_path(base + '/' + file, file_path, False) 156 | if fullpath: 157 | return fullpath 158 | continue 159 | 160 | fullpath = os.path.join(base, file_path) 161 | if os.path.isfile(fullpath): 162 | return fullpath 163 | 164 | 165 | def get_folders(): 166 | return sublime.active_window().folders() 167 | 168 | 169 | def class_2_file(class_name): 170 | ''' 171 | convert PHP class name to filename 172 | ''' 173 | filename = class_name.replace(',', '').replace('::class', '') 174 | filename = filename.replace('\\', '/').strip() + '.php' 175 | if filename.startswith('/'): 176 | filename = filename[1:] 177 | 178 | if filename.startswith('App/'): 179 | filename = filename.replace('App/', 'app/', 1) 180 | 181 | return filename 182 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sublime 3 | import sublime_plugin 4 | from os.path import basename 5 | 6 | if int(sublime.version()) >= 3114: 7 | 8 | # Clear module cache to force reloading all modules of this package. 9 | # See https://github.com/emmetio/sublime-text-plugin/issues/35 10 | prefix = __package__ + "." # don't clear the base package 11 | for module_name in [ 12 | module_name 13 | for module_name in sys.modules 14 | if module_name.startswith(prefix) and module_name != __name__ 15 | ]: 16 | del sys.modules[module_name] 17 | prefix = None 18 | 19 | from .lib.selection import Selection 20 | from .lib.finder import get_place 21 | from .lib.setting import Setting 22 | from .lib.router import Router 23 | 24 | place = None 25 | 26 | 27 | class LaravelGotoCommand(sublime_plugin.TextCommand): 28 | def __init__(self, view): 29 | super().__init__(view) 30 | 31 | def run(self, edit): 32 | global place 33 | selection = Selection(self.view) 34 | place = get_place(selection) 35 | goto_place(place) 36 | 37 | def is_visible(self): 38 | filename = self.view.file_name() 39 | return bool(filename and ( 40 | filename.endswith('.php') or 41 | filename.endswith('.js') or 42 | filename.endswith('.ts') or 43 | filename.endswith('.jsx') or 44 | filename.endswith('.vue') 45 | ) 46 | ) 47 | 48 | 49 | class GotoControllerCommand(sublime_plugin.WindowCommand): 50 | uris = [] 51 | 52 | def run(self): 53 | router = Router() 54 | self.uris = router.uris() 55 | items = [] 56 | for uri in self.uris: 57 | item = sublime.QuickPanelItem(uri.label, uri.detail) 58 | items.append(item) 59 | 60 | self.window.show_quick_panel( 61 | items, 62 | self.on_done, 63 | sublime.MONOSPACE_FONT 64 | ) 65 | 66 | def on_done(self, index): 67 | if index == -1: 68 | return # User cancelled the selection 69 | 70 | uri = self.uris[index] 71 | goto_place(uri.place) 72 | 73 | 74 | class GotoLocation(sublime_plugin.EventListener): 75 | def on_load(self, view): 76 | global place 77 | filepath = view.file_name() 78 | if (not place or not filepath): 79 | place = None 80 | return 81 | if (basename(filepath) != basename(place.path)): 82 | found = False 83 | for path in place.paths: 84 | if filepath.endswith(path): 85 | found = True 86 | break 87 | if not found: 88 | place = None 89 | return 90 | if (not isinstance(place.location, str)): 91 | place = None 92 | return 93 | spot_location(view, place, filepath) 94 | 95 | def on_post_save_async(self, view): 96 | Router().update(view.file_name()) 97 | 98 | def on_hover(self, view, point, hover_zone): 99 | if view.is_popup_visible(): 100 | return 101 | if sublime.HOVER_TEXT != hover_zone: 102 | return 103 | if not Setting().get('show_hover'): 104 | return 105 | global place 106 | selection = Selection(view, point) 107 | place = get_place(selection) 108 | 109 | if place and place.path: 110 | content = self.build_link(place.path) 111 | 112 | if place.paths: 113 | content = '
'.join(map(self.build_link, place.paths)) 114 | if place.uris: 115 | content += '

' +\ 116 | self.build_link( 117 | 'Open all files above in new window', 118 | 'A!!' 119 | ) 120 | 121 | view.show_popup( 122 | content, 123 | flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, 124 | location=point, 125 | max_width=640, 126 | on_navigate=self.on_navigate 127 | ) 128 | 129 | def build_link(self, path, href=None): 130 | if not href: 131 | href = path 132 | 133 | return '' + path + '' 134 | 135 | def on_navigate(self, link): 136 | global place 137 | 138 | if link == 'A!!' and place.uris: 139 | open_file_layouts(place.uris) 140 | return 141 | if place.paths and link in place.paths: 142 | place.path = link 143 | place.paths = [] 144 | 145 | goto_place(place) 146 | 147 | 148 | def goto_place(place): 149 | if place is None: 150 | sublime.status_message('Laravel Goto: unidentified string.') 151 | return 152 | 153 | window = sublime.active_window() 154 | 155 | if place.paths: 156 | if place.uris: 157 | place.paths.append('Open all files above in new window') 158 | window.show_quick_panel( 159 | place.paths, 160 | on_path_select 161 | ) 162 | return 163 | 164 | if place.uri: 165 | window.open_file(place.uri) 166 | return 167 | 168 | args = { 169 | "overlay": "goto", 170 | "show_files": True, 171 | "text": place.path 172 | } 173 | 174 | if place.is_controller: 175 | args["text"] = '' 176 | window.run_command("show_overlay", args) 177 | window.run_command("insert", { 178 | "characters": place.path 179 | }) 180 | return 181 | 182 | window.run_command("show_overlay", args) 183 | 184 | 185 | def on_path_select(idx): 186 | if -1 == idx: 187 | return 188 | 189 | if place.uris and place.paths[idx] == place.paths[-1]: 190 | open_file_layouts(place.uris) 191 | return 192 | 193 | place.path = place.paths[idx] 194 | place.paths = [] 195 | goto_place(place) 196 | 197 | 198 | def open_file_layouts(files=[]): 199 | '''open files in multi-columns layouts''' 200 | width = 1 / len(files) 201 | cols = [0.0] 202 | cells = [] 203 | for (idx, file) in enumerate(files): 204 | cols.append(width*idx+width) 205 | cells.append([idx, 0, idx+1, 1]) 206 | 207 | active_window = sublime.active_window() 208 | active_window.run_command('new_window') 209 | new_window = sublime.active_window() 210 | new_window.set_layout({ 211 | "cols": cols, 212 | "rows": [0.0, 1.0], 213 | "cells": cells 214 | }) 215 | for (idx, file) in enumerate(files): 216 | new_window.open_file(file) 217 | new_window.set_view_index(new_window.active_view(), idx, 0) 218 | return 219 | 220 | 221 | def spot_location(view, place, filepath): 222 | ''' spot place location on view ''' 223 | if not place.location: 224 | return 225 | 226 | location = place.location 227 | filename = basename(filepath) 228 | # print(filename) 229 | if filename in place.locations: 230 | location = place.locations[filename] 231 | 232 | found = view.find(location, 0) 233 | # fix .env not showing selected if no scrolling happened 234 | view.set_viewport_position((0, 1)) 235 | view.sel().clear() 236 | view.sel().add(found) 237 | view.show(found) 238 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Goto 2 | 3 | [![Package Control Downloads](https://img.shields.io/packagecontrol/dt/Laravel%20Goto?style=for-the-badge)](https://packagecontrol.io/packages/Laravel%20Goto) 4 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/absszero/LaravelGoto/test.yml?style=for-the-badge) 5 | 6 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/absszero) 7 | 8 | Goto various Laravel files 9 | 10 | ![example]gifs/(example.gif) 11 | 12 | ## Features 13 | 14 | ### Go to Blade 15 | 16 | Go to blade template files. 17 | 18 | ```php 19 | view('hello_view', ['name' => 'James']); 20 | 21 | Route::view('/', 'pages.public.index'); 22 | 23 | @includeIf('view.name', ['status' => 'complete']) 24 | 25 | @each('view.name', $jobs, 'job', 'view.empty') 26 | 27 | @extends('layouts.app') 28 | ``` 29 | 30 | Go to blade Component files. 31 | 32 | ```php 33 | 34 | ``` 35 | 36 | ### Go to Controller 37 | 38 | Go to controllers and highlight method. 39 | 40 | ```php 41 | Route::get('/', 'HelloController@index'); 42 | 43 | Route::resource('photo', 'HelloController', ['only' => [ 44 | 'index', 'show' 45 | ]]); 46 | ``` 47 | 48 | ### Go to Controller via Uris 49 | 50 | Go to the controller via the "Laravel Goto: Go to Controller via Uris" command. 51 | 52 | ![](gifs/controller.gif) 53 | 54 | ### Go to Controller from route helper 55 | 56 | ![](gifs/route.gif) 57 | 58 | ### Go to Middleware 59 | 60 | ![](gifs/middleware.gif) 61 | 62 | ### Go to Config 63 | 64 | Go to config files and highlight option. 65 | 66 | ```php 67 | Config::get('app.timezone'); 68 | Config::set('app.timezone', 'UTC'); 69 | ``` 70 | 71 | ### Go to Filesystem config 72 | 73 | Go to filesystem config file and highlight option. 74 | 75 | ```php 76 | Storage::disk('local')->put('example.txt', 'Contents'); 77 | ``` 78 | 79 | ### Go to Language 80 | 81 | Go to single language file or open all and highlight option. 82 | 83 | ![](gifs/language.gif) 84 | 85 | ### Go to .env 86 | 87 | ``` 88 | env('APP_DEBUG', false); 89 | ``` 90 | 91 | ### Go to Command 92 | 93 | ![](gifs/command.gif) 94 | 95 | 96 | ### Go to Inertia.js 97 | 98 | ```php 99 | Route::inertia('/about', 'About/AboutComponent'); 100 | 101 | Inertia::render('MyComponent'); 102 | 103 | inertia('About/AboutComponent'); 104 | ``` 105 | 106 | ### Go to Livewire 107 | 108 | ```php 109 | @livewire('nav.show-post') 110 | 111 | 112 | ``` 113 | 114 | ### Go to path helper 115 | 116 | ```php 117 | app_path('User.php'); 118 | 119 | base_path('vendor'); 120 | 121 | config_path('app.php'); 122 | 123 | database_path('UserFactory.php'); 124 | 125 | public_path('css/app.css'); 126 | 127 | resource_path('sass/app.scss'); 128 | 129 | storage_path('logs/laravel.log'); 130 | ``` 131 | 132 | ### Go to Static files 133 | 134 | ```php 135 | $file = 'js/hello.js'; 136 | ``` 137 | 138 | Default supported static file extensions: 139 | 140 | - js 141 | - ts 142 | - jsx 143 | - vue 144 | - css 145 | - scss 146 | - sass 147 | - less 148 | - styl 149 | - htm 150 | - html 151 | - xhtml 152 | - xml 153 | - log 154 | 155 | 156 | ## Installation 157 | 158 | ### Package Control 159 | 160 | 1. `Ctrl+Shift+P` then select `Package Control: Install Package` 161 | 2. Type `Laravel Goto` 162 | 163 | ### Manually 164 | 165 | - MacOS 166 | 167 | ```shell 168 | git clone https://github.com/absszero/LaravelGoto.git ~/Library/Application\ Support/Sublime\ Text\ 3/Packages/LaravelGoto 169 | ``` 170 | 171 | - Linux 172 | 173 | ```shell 174 | git clone https://github.com/absszero/LaravelGoto.git ~/.config/sublime-text-3/Packages/LaravelGoto 175 | ``` 176 | 177 | - Windows 178 | 179 | ```shell 180 | git clone https://github.com/absszero/LaravelGoto.git %APPDATA%\Sublime Text 3\Packages\LaravelGoto 181 | ``` 182 | 183 | 184 | 185 | ## Usage 186 | 187 | - Select a text, `Right-Click` to open content menu, Press `Laravel Goto` or use Alt + ;. 188 | 189 | 190 | ## Settings 191 | 192 | ### PHP bin 193 | 194 | ```json 195 | "php_bin": "c:\\php\\php.exe" 196 | ``` 197 | 198 | ### Show hover popup if available 199 | 200 | ```json 201 | "show_hover": true 202 | ``` 203 | 204 | ### Extend static file extensions 205 | 206 | You can add other file extensions throught `Preferences > Package Settings > LaravelGoto > Settings`, and add this option `static_extensions` 207 | 208 | ```json 209 | "static_extensions": [ 210 | "your_extension_here" 211 | ] 212 | ``` 213 | --------------------------------------------------------------------------------