├── .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 | [](https://packagecontrol.io/packages/Laravel%20Goto)
4 | 
5 |
6 | [](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 | 
53 |
54 | ### Go to Controller from route helper
55 |
56 | 
57 |
58 | ### Go to Middleware
59 |
60 | 
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 | 
84 |
85 | ### Go to .env
86 |
87 | ```
88 | env('APP_DEBUG', false);
89 | ```
90 |
91 | ### Go to Command
92 |
93 | 
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 |
--------------------------------------------------------------------------------