ソースを参照

Added object and framework function autocomplete
Keywords of the object such as the following are implemented:
- $this->
- self::
- static::
- $object->
- class::
Framework functions have also been added. PHP functions are still missing as they require a documentation "stub".

Tim Fry 4 ヶ月 前
コミット
f32b7d94bb

+ 0 - 2
app_config.php

@@ -180,5 +180,3 @@
 		$apps[$x]['db'][$y]['fields'][$z]['type']['sqlite'] = "text";
 		$apps[$x]['db'][$y]['fields'][$z]['type']['mysql'] = "char(36)";
 		$apps[$x]['db'][$y]['fields'][$z]['description']['en-us'] = "";
-
-?>

+ 0 - 2
app_languages.php

@@ -815,5 +815,3 @@ $text['label-file']['uk-ua'] = "Файл:";
 $text['label-file']['zh-cn'] = "文件:";
 $text['label-file']['ja-jp'] = "ファイル:";
 $text['label-file']['ko-kr'] = "파일:";
-
-?>

+ 0 - 2
app_menu.php

@@ -114,5 +114,3 @@
 	$apps[$x]['menu'][$y]['path'] = "/app/edit/index.php?dir=php";
 	$apps[$x]['menu'][$y]['groups'][] = "superadmin";
 	$y++;
-
-?>

+ 0 - 2
clip_add.php

@@ -133,5 +133,3 @@ if (count($_POST)>0) {
 
 //include the footer
 	require_once "footer.php";
-
-?>

+ 0 - 2
clip_delete.php

@@ -63,5 +63,3 @@
 
 //redirect the browser
 	header("Location: clip_options.php");
-
-?>

+ 0 - 2
clip_list.php

@@ -226,5 +226,3 @@
 
 //inclue the footer
 require_once "footer.php";
-
-?>

+ 0 - 2
clip_options.php

@@ -71,5 +71,3 @@
 
 //include footer
 	require_once "footer.php";
-
-?>

+ 0 - 2
clip_options_list.php

@@ -182,5 +182,3 @@
 
 //include the footer
 	require_once "footer.php";
-
-?>

+ 0 - 2
clip_update.php

@@ -155,5 +155,3 @@
 
 //include the footer
 	require_once "footer.php";
-
-?>

+ 0 - 1
file_delete.php

@@ -108,4 +108,3 @@
 		//include the footer
 		require_once "footer.php";
 	}
-?>

+ 16 - 99
file_list.php

@@ -69,7 +69,7 @@
 					else {
 						$dir_array[] = $newpath;
 					}
-					if ($x > 1000) { break; };
+					if ($x > 1000) { break; }
 					$x++;
 				}
 			}
@@ -92,7 +92,7 @@
 				$newpath = str_replace ('//', '/', $newpath);
 				$newpath = str_replace ("\\", "/", $newpath);
 				$html_file_list .= "<div style='white-space: nowrap; padding-left: 16px;'>\n";
-				$html_file_list .= "<a href='javascript:void(0);' onclick=\"parent.document.getElementById('filepath').value='".$newpath."'; parent.document.getElementById('current_file').value = '".$newpath."'; makeRequest('file_read.php','file=".urlencode($newpath)."');\" title='".$newpath." &#10; ".$filesize." KB'>";
+				$html_file_list .= "<a href='javascript:void(0);' onclick=\"document.getElementById('filepath').value='".$newpath."'; document.getElementById('current_file').value = '".$newpath."'; makeRequest('file_read.php','file=".urlencode($newpath)."');\" title='".$newpath." &#10; ".$filesize." KB'>";
 				$html_file_list .= "<img src='resources/images/icon_file.png' border='0' align='absmiddle' style='margin: 1px 2px 3px -1px;'>".$filename."</a>\n";
 				$html_file_list .= "</div>\n";
 			}
@@ -172,99 +172,15 @@
 	}
 
 
-
-//define ajax functions
-echo "<script type=\"text/javascript\" language=\"javascript\">\n";
-echo "    function makeRequest(url, strpost) {\n";
-echo "        var http_request = false;\n";
-echo "\n";
-echo "        if (window.XMLHttpRequest) { // Mozilla, Safari, ...\n";
-echo "            http_request = new XMLHttpRequest();\n";
-echo "            if (http_request.overrideMimeType) {\n";
-echo "                http_request.overrideMimeType('text/xml');\n";
-echo "                // See note below about this line\n";
-echo "            }\n";
-echo "        } else if (window.ActiveXObject) { // IE\n";
-echo "            try {\n";
-echo "                http_request = new ActiveXObject(\"Msxml2.XMLHTTP\");\n";
-echo "            } catch (e) {\n";
-echo "                try {\n";
-echo "                    http_request = new ActiveXObject(\"Microsoft.XMLHTTP\");\n";
-echo "                } catch (e) {}\n";
-echo "            }\n";
-echo "        }\n";
-echo "\n";
-echo "        if (!http_request) {\n";
-echo "            alert('".$text['message-give-up']."');\n";
-echo "            return false;\n";
-echo "        }\n";
-echo "        http_request.onreadystatechange = function() { returnContent(http_request); };\n";
-echo "        if (http_request.overrideMimeType) {\n";
-echo "              http_request.overrideMimeType('text/html');\n";
-echo "        }\n";
-echo "        http_request.open('POST', url, true);\n";
-echo "\n";
-echo "\n";
-echo "        if (strpost.length == 0) {\n";
-echo "            //http_request.send(null);\n";
-echo "            http_request.send('name=value&foo=bar');\n";
-echo "        }\n";
-echo "        else {\n";
-echo "            http_request.setRequestHeader('Content-Type','application/x-www-form-urlencoded');\n";
-echo "            http_request.send(strpost);\n";
-echo "        }\n";
-echo "\n";
-echo "    }\n";
-echo "\n";
-echo "    function returnContent(http_request) {\n";
-echo "\n";
-echo "        if (http_request.readyState == 4) {\n";
-echo "            if (http_request.status == 200) {\n";
-echo "			parent.document.getElementById('editor_source').value=http_request.responseText;";
-echo "			parent.editor.getSession().setValue(parent.document.getElementById('editor_source').value);";
-echo "			parent.editor.gotoLine(1);";
-echo "			parent.editor.scrollToLine(1, true, true, function() {});";
-echo "			parent.editor.focus();";
-echo "\n";
-echo "            }\n";
-echo "            else {\n";
-echo "                alert('".$text['message-problem']."');\n";
-echo "            }\n";
-echo "        }\n";
-echo "\n";
-echo "    }\n";
-echo "</script>";
-
-
-echo "<SCRIPT LANGUAGE=\"JavaScript\">\n";
-//echo "// ---------------------------------------------\n";
-//echo "// --- http://www.codeproject.com/jscript/dhtml_treeview.asp\n";
-//echo "// --- Name:    Easy DHTML Treeview           --\n";
-//echo "// --- Author:  D.D. de Kerf                  --\n";
-//echo "// --- Version: 0.2          Date: 13-6-2001  --\n";
-//echo "// ---------------------------------------------\n";
-echo "function Toggle(node) {\n";
-echo "	// Unfold the branch if it isn't visible\n";
-echo "	if (node.nextSibling.style.display == 'none') {\n";
-echo "  	node.nextSibling.style.display = 'block';\n";
-echo "	}\n";
-echo "	// Collapse the branch if it IS visible\n";
-echo "	else {\n";
-echo "  	node.nextSibling.style.display = 'none';\n";
-echo "	}\n";
-echo "\n";
-echo "}\n";
-echo "</SCRIPT>";
-
 // keyboard shortcut bindings
-echo "<script language='JavaScript' type='text/javascript' src='".PROJECT_PATH."/resources/jquery/jquery-3.6.1.min.js'></script>\n";
+echo "<script src='".PROJECT_PATH."/resources/jquery/jquery-3.6.1.min.js'></script>\n";
 echo "<script src='https://code.jquery.com/jquery-migrate-3.1.0.js'></script>\n";
 
 //save file
-key_press('ctrl+s', 'down', 'window', null, null, "parent.$('form#frm_edit').submit(); return false;", true);
+key_press('ctrl+s', 'down', 'window', null, null, "$('form#frm_edit').submit(); return false;", true);
 
 //open file manager/clip library pane
-key_press('ctrl+q', 'down', 'window', null, null, 'parent.toggle_sidebar(); parent.focus_editor(); return false;', true);
+key_press('ctrl+q', 'down', 'window', null, null, 'toggle_sidebar(); focus_editor(); return false;', true);
 
 //prevent backspace (browser history back)
 key_press('backspace', 'down', 'window', null, null, 'return false;', true);
@@ -274,17 +190,18 @@ echo "<body style='margin: 0px; padding: 5px;'>\n";
 
 echo "<div style='text-align: left; padding-top: 3px; padding-bottom: 3px;'><a href='javascript:void(0);' onclick=\"window.open('file_options.php','filewin','left=20,top=20,width=310,height=350,toolbar=0,resizable=0');\" style='text-decoration:none;' title='".$text['label-files']."'><img src='resources/images/icon_gear.png' border='0' align='absmiddle' style='margin: 0px 2px 4px -1px;'>".$text['label-files']."</a></div>\n";
 echo "<div style='text-align: left; margin-left: -16px;'>\n";
-if (file_exists($edit_directory)) {
-	echo recur_dir($edit_directory);
+if (function_exists('apcu_enabled') && apcu_enabled() && apcu_exists('edit_html_list')) {
+	echo apcu_fetch('edit_html_list');
+	exit();
 }
-echo "</div>\n";
 
-require_once "footer.php";
+if (file_exists($edit_directory)) {
+	$edit_html_list = recur_dir($edit_directory);
 
-unset ($result_count);
-unset ($result);
-unset ($key);
-unset ($val);
-unset ($c);
+	if (function_exists('apcu_enabled') && apcu_enabled()) {
+		apcu_store('edit_html_list', $edit_html_list); // only available for 5 minutes
+	}
+	echo $edit_html_list;
+}
 
-?>
+echo "</div>\n";

+ 0 - 2
file_new.php

@@ -114,5 +114,3 @@
 
 		require_once "footer.php";
 	}
-
-?>

+ 0 - 2
file_options.php

@@ -85,5 +85,3 @@
 
 //include the footer
 	require_once "footer.php";
-
-?>

+ 0 - 2
file_options_list.php

@@ -268,5 +268,3 @@ if (!isset($_SESSION)) { session_start(); }
 echo "</div>\n";
 
 require_once "footer.php";
-
-?>

+ 0 - 2
file_read.php

@@ -138,5 +138,3 @@
 		*/
 
 	}
-
-?>

+ 0 - 2
file_rename.php

@@ -124,5 +124,3 @@
 		require_once "footer.php";
 	
 	}
-
-?>

+ 0 - 2
file_save.php

@@ -173,5 +173,3 @@
 			}
 		}
 	}
-
-?>

+ 0 - 2
folder_delete.php

@@ -93,5 +93,3 @@
 		//include the footer
 		require_once "footer.php";
 	}
-
-?>

+ 0 - 2
folder_new.php

@@ -113,5 +113,3 @@
 		//show the footer
 		require_once "footer.php";
 	}
-
-?>

+ 0 - 2
footer.php

@@ -27,5 +27,3 @@
 echo "<div>";
 echo "</body>";
 echo "</html>";
-
-?>

+ 2 - 4
header.php

@@ -71,7 +71,7 @@ echo "//-->\n";
 echo "</style>\n";
 
 
-echo "<SCRIPT language=\"JavaScript\">\n";
+echo "<script>\n";
 echo "<!--\n";
 echo "function confirmdelete(url)\n";
 echo "{\n";
@@ -81,9 +81,7 @@ echo "      window.location=url;\n";
 echo " }\n";
 echo "}\n";
 echo "//-->\n";
-echo "</SCRIPT>\n";
+echo "</script>\n";
 echo "</head>\n";
 echo "<body style='margin: 0; padding: 5px;'>\n";
 echo "<div align='center'>\n";
-
-?>

+ 92 - 41
index.php

@@ -373,6 +373,52 @@
     // Remove unwanted shortcuts
     editor.commands.bindKey("Ctrl-T", null); // Disable new browser tab shortcut
 
+// Levenshtein distance algorithm
+function levenshteinDistance(a, b) {
+	let m = a.length, n = b.length;
+	let dp = [];
+	for (let i = 0; i <= m; i++) {
+		dp[i] = [i];
+	}
+	for (let j = 0; j <= n; j++) {
+		dp[0][j] = j;
+	}
+	for (let i = 1; i <= m; i++) {
+		for (let j = 1; j <= n; j++) {
+			if (a[i - 1] === b[j - 1]) {
+				dp[i][j] = dp[i - 1][j - 1];
+			} else {
+				dp[i][j] = Math.min(
+					dp[i - 1][j] + 1,    // deletion
+					dp[i][j - 1] + 1,    // insertion
+					dp[i - 1][j - 1] + 1 // substitution
+				);
+			}
+		}
+	}
+	return dp[m][n];
+}
+
+// Example function to find the closest matching class key
+function findClosestMatch(refName, phpMethods) {
+	let bestMatch = null;
+	let bestDistance = Infinity;
+	let lowerRef = refName.toLowerCase();
+
+	// Loop through all classes in phpMethods
+	for (let key in phpMethods) {
+		// Assume the simple class name is the last segment after a backslash
+		let parts = key.split("\\");
+		let simpleName = parts[parts.length - 1].toLowerCase();
+		let distance = levenshteinDistance(lowerRef, simpleName);
+		if (distance < bestDistance) {
+			bestDistance = distance;
+			bestMatch = key;
+		}
+	}
+	return bestMatch;
+}
+
 // Function to fetch PHP class methods using fetch() with promises
 async function fetch_php_methods() {
 	try {
@@ -387,79 +433,84 @@ async function fetch_php_methods() {
 
 // Initialize ACE auto-completion after fetching PHP methods
 async function init_ace_completion() {
-	let phpMethods = await fetch_php_methods();
+	const php_methods = await fetch_php_methods();
 
 	// Custom completer for PHP class methods
 	var php_class_completer = {
 		getCompletions: function(editor, session, pos, prefix, callback) {
+
+			// Define current_class_name by extracting the basename of the current file without extension.
+			var current_file_path = document.getElementById('current_file').value;
+			var current_file_name = current_file_path.split('/').pop();
+
+			// Remove the extension (everything after the last dot)
+			var current_class_name = current_file_name.replace(/\.[^/.]+$/, "");
+
 			// Get the current line text
 			var line = session.getLine(pos.row);
 
-			// Use regex to detect object (->) or static (::) access
-			const objectMatch = line.match(/(\w+)\s*->\s*\w*$/);
-			const staticMatch = line.match(/(\w+)::\w*$/);
-
-			// Extract the referenced class name (simple name)
-			var ref_name = objectMatch ? objectMatch[1] : (staticMatch ? staticMatch[1] : null);
-			if (!ref_name) return callback(null, []);
+			// Use regex to detect object (->) or static (::) access.
+			// This regex captures either "$this" or any other word.
+			const object_match = line.match(/(\$this|\w+)\s*->\s*\w*$/);
+			const static_match = line.match(/(self|static|\w+)::\w*$/);
 
-			// Try to match the simple class name (case-insensitive) with one of the keys in phpMethods.
-			// The keys in phpMethods may be fully-qualified names (with namespaces).
-			var matched_class = null;
-			for (var key in phpMethods) {
-				// Get the simple class name from the key
-				var parts = key.split("\\");
-				var simple_name = parts[parts.length - 1];
-				if (simple_name.toLowerCase() === ref_name.toLowerCase()) {
-					matched_class = key;
-					break;
-				}
+			// Extract the referenced name; if it's "$this", use the current_class_name.
+			var ref_name = object_match ? object_match[1] : (static_match ? static_match[1] : null);
+			if (ref_name === '$this' | ref_name === 'self' | ref_name === 'static') {
+				ref_name = current_class_name;
 			}
 
-			// If no matching class is found, return an empty list.
+			// If not a class, maybe a user function; use the prefix.
+			if (prefix.length > 0) ref_name = prefix;
+
+			if (!ref_name) return callback(null, []);
+
+			// Find the closest matching class using fuzzy matching.
+			var matched_class = findClosestMatch(ref_name, php_methods);
 			if (!matched_class) return callback(null, []);
 
 			// Map the methods of the matched class into completions.
-			var completions = phpMethods[matched_class].map(function(method) {
-				if (staticMatch !== null) {
-					if (method.static) {
-						return {
-							caption: method.name + method.params,
-							snippet: method.name + method.params.replace(/\$/g, "\\$"),
-							meta: matched_class,
-							docHTML: method.doc ? method.doc : "No Documentation"
-						};
-					} else {
-						return {};
-					}
+			var completions = php_methods[matched_class].map(function(method) {
+				// If static syntax is used but the method is not static, skip it.
+				if (static_match !== null && !method.static) {
+					return {};
 				}
-				//you can call a static method on an object instance because php is like that
-				return {
+
+				// Map the item to the documentation
+				var item = {
 					caption: method.name + method.params,
 					snippet: method.name + method.params.replace(/\$/g, "\\$"),
-					meta: matched_class,
+					meta: method.meta,
 					docHTML: method.doc ? method.doc : "No Documentation"
 				};
+
+				// TODO: fix italics
+				// Use italics in pop-up for static methods
+				if (method.static) {
+					item.className = "ace_static_method";
+				}
+
+				//return the mapped json object
+				return item;
 			});
 
 			callback(null, completions);
 		}
 	};
 
-	// Initialize ACE Editor (assumes 'editor' is already created)
+	// Initialize ACE Editor
 	ace.require("ace/ext/language_tools");
 
-	// Override the default completions with our custom completer
+	// Replace the current list of completions with our custom one so we don't have every single word in the document listed as a completion option
 	editor.completers = [php_class_completer];
 
 	// Ensure font size is set
-    document.getElementById('editor').style.fontSize='<?=$setting_size?>';
-    focus_editor();
-
+	document.getElementById('editor').style.fontSize = '<?=$setting_size?>';
+	focus_editor();
 }
 
 // Run auto-completion setup
-init_ace_completion();
+	init_ace_completion();
 </script>
 </body>
 <script>

+ 28 - 0
resources/get_contents.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * FusionPBX
+ * Version: MPL 1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is FusionPBX
+ *
+ * The Initial Developer of the Original Code is
+ * Mark J Crane <[email protected]>
+ * Portions created by the Initial Developer are Copyright (C) 2008-2025
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mark J Crane <[email protected]>
+ * Tim Fry <[email protected]>
+ */
+