diff --git a/Neon Vision Editor/ContentView+Toolbar.swift b/Neon Vision Editor/ContentView+Toolbar.swift index 16f6f26..0dbe409 100644 --- a/Neon Vision Editor/ContentView+Toolbar.swift +++ b/Neon Vision Editor/ContentView+Toolbar.swift @@ -42,9 +42,10 @@ extension ContentView { @ViewBuilder private var languagePickerControl: some View { Picker("Language", selection: currentLanguageBinding) { - ForEach(["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in + ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in let label: String = { switch lang { + case "php": return "PHP" case "objective-c": return "Objective-C" case "csharp": return "C#" case "cpp": return "C++" @@ -52,6 +53,7 @@ extension ContentView { case "xml": return "XML" case "yaml": return "YAML" case "toml": return "TOML" + case "csv": return "CSV" case "ini": return "INI" case "sql": return "SQL" case "html": return "HTML" @@ -286,9 +288,10 @@ extension ContentView { #else ToolbarItemGroup(placement: .automatic) { Picker("Language", selection: currentLanguageBinding) { - ForEach(["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in + ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in let label: String = { switch lang { + case "php": return "PHP" case "objective-c": return "Objective‑C" case "csharp": return "C#" case "cpp": return "C++" @@ -296,6 +299,7 @@ extension ContentView { case "xml": return "XML" case "yaml": return "YAML" case "toml": return "TOML" + case "csv": return "CSV" case "ini": return "INI" case "sql": return "SQL" case "html": return "HTML" diff --git a/Neon Vision Editor/ContentView.swift b/Neon Vision Editor/ContentView.swift index 5c06612..09a55b6 100644 --- a/Neon Vision Editor/ContentView.swift +++ b/Neon Vision Editor/ContentView.swift @@ -873,7 +873,7 @@ struct ContentView: View { /// Returns a supported language string used by syntax highlighting and the language picker. private func detectLanguageWithAppleIntelligence(_ text: String) async -> String { // Supported languages in our picker - let supported = ["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "objective-c", "csharp", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"] + let supported = ["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "objective-c", "csharp", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"] #if USE_FOUNDATION_MODELS // Attempt a lightweight model-based detection via AppleIntelligenceAIClient if available @@ -896,6 +896,18 @@ struct ContentView: View { if lower.contains("c#") || lower.contains("c sharp") || lower.range(of: #"\bcs\b"#, options: .regularExpression) != nil || lower.contains(".cs") { return "csharp" } + if lower.contains("") || lower.contains("$_get") || lower.contains("$_post") || lower.contains("$_server") { + return "php" + } + if text.contains(",") && text.contains("\n") { + let lines = text.split(separator: "\n", omittingEmptySubsequences: true) + if lines.count >= 2 { + let commaCounts = lines.prefix(6).map { line in line.filter { $0 == "," }.count } + if let firstCount = commaCounts.first, firstCount > 0 && commaCounts.dropFirst().allSatisfy({ $0 == firstCount || abs($0 - firstCount) <= 1 }) { + return "csv" + } + } + } // C# strong heuristic if lower.contains("using system") || lower.contains("namespace ") || lower.contains("public class") || lower.contains("public static void main") || lower.contains("static void main") || lower.contains("console.writeline") || lower.contains("console.readline") || lower.contains("class program") || lower.contains("get; set;") || lower.contains("list<") || lower.contains("dictionary<") || lower.contains("ienumerable<") || lower.range(of: #"\[[A-Za-z_][A-Za-z0-9_]*\]"#, options: .regularExpression) != nil { return "csharp" diff --git a/Neon Vision Editor/EditorViewModel.swift b/Neon Vision Editor/EditorViewModel.swift index 499640a..5ce6f77 100644 --- a/Neon Vision Editor/EditorViewModel.swift +++ b/Neon Vision Editor/EditorViewModel.swift @@ -35,6 +35,10 @@ class EditorViewModel: ObservableObject { "swift": "swift", "py": "python", "js": "javascript", + "php": "php", + "phtml": "php", + "csv": "csv", + "tsv": "csv", "html": "html", "css": "css", "c": "c", diff --git a/Neon Vision Editor/LanguageDetector.swift b/Neon Vision Editor/LanguageDetector.swift index 63ecdf0..d660a0e 100644 --- a/Neon Vision Editor/LanguageDetector.swift +++ b/Neon Vision Editor/LanguageDetector.swift @@ -15,6 +15,10 @@ public struct LanguageDetector { "swift": "swift", "py": "python", "js": "javascript", + "php": "php", + "phtml": "php", + "csv": "csv", + "tsv": "csv", "ts": "javascript", "html": "html", "css": "css", @@ -69,6 +73,8 @@ public struct LanguageDetector { var scores: [String: Int] = [ "swift": 0, "csharp": 0, + "php": 0, + "csv": 0, "python": 0, "javascript": 0, "cpp": 0, @@ -98,12 +104,23 @@ public struct LanguageDetector { if t.contains("```swift") { bump("swift", 100) } if t.contains("```python") { bump("python", 100) } if t.contains("```js") || t.contains("```javascript") { bump("javascript", 100) } + if t.contains("```php") { bump("php", 100) } if t.contains("```csharp") || t.contains("```cs") { bump("csharp", 100) } if t.contains("```cpp") || t.contains("```c++") { bump("cpp", 100) } // 2) Single-language quick checks if let first = trimmed.first, (first == "{" || first == "[") && t.contains(":") { bump("json", 90) } if t.contains("= 2 { + let commaCounts = lines.prefix(6).map { line in line.filter { $0 == "," }.count } + if let firstCount = commaCounts.first, firstCount > 0 && commaCounts.dropFirst().allSatisfy({ $0 == firstCount || abs($0 - firstCount) <= 1 }) { + bump("csv", 80) + } + } + } if t.contains("#!/bin/bash") || t.contains("#!/usr/bin/env bash") { bump("bash", 90) } if t.contains("#!/bin/zsh") || t.contains("#!/usr/bin/env zsh") { bump("zsh", 90) } @@ -181,19 +198,27 @@ public struct LanguageDetector { if t.contains("\ndef ") || t.hasPrefix("def ") { bump("python", 15) } if t.contains("\nimport ") && t.contains(":\n") { bump("python", 8) } - // 6) JavaScript / TypeScript + // 6) PHP + if t.contains("$this->") || t.contains("$_get") || t.contains("$_post") || t.contains("$_server") || t.contains("$_session") { + bump("php", 20) + } + if (t.contains("function ") && t.contains("$")) || t.contains("echo ") { + bump("php", 10) + } + + // 7) JavaScript / TypeScript if t.contains("function ") || t.contains("=>") || t.contains("console.log") { bump("javascript", 15) } - // 7) C/C++ + // 8) C/C++ if t.contains("#include") || t.contains("std::") { bump("cpp", 20) } if t.contains("int main(") { bump("cpp", 8) } - // 8) CSS + // 9) CSS if t.contains("{") && t.contains("}") && t.contains(":") && t.contains(";") && !t.contains("func ") { bump("css", 8) } - // 9) Markdown + // 10) Markdown if t.contains("\n# ") || t.hasPrefix("# ") || t.contains("\n- ") || t.contains("\n* ") { bump("markdown", 8) } // Conflict resolution tweaks diff --git a/Neon Vision Editor/NeonVisionEditorApp.swift b/Neon Vision Editor/NeonVisionEditorApp.swift index bb92b35..a4715ea 100644 --- a/Neon Vision Editor/NeonVisionEditorApp.swift +++ b/Neon Vision Editor/NeonVisionEditorApp.swift @@ -159,8 +159,27 @@ struct NeonVisionEditorApp: App { } CommandMenu("Language") { - ForEach(["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in - Button(lang.capitalized) { + ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in + let label: String = { + switch lang { + case "php": return "PHP" + case "objective-c": return "Objective-C" + case "csharp": return "C#" + case "cpp": return "C++" + case "json": return "JSON" + case "xml": return "XML" + case "yaml": return "YAML" + case "toml": return "TOML" + case "csv": return "CSV" + case "ini": return "INI" + case "sql": return "SQL" + case "html": return "HTML" + case "css": return "CSS" + case "standard": return "Standard" + default: return lang.capitalized + } + }() + Button(label) { if let tab = viewModel.selectedTab { viewModel.updateTabLanguage(tab: tab, language: lang) } diff --git a/Neon Vision Editor/SidebarViews.swift b/Neon Vision Editor/SidebarViews.swift index 2506f44..3335d88 100644 --- a/Neon Vision Editor/SidebarViews.swift +++ b/Neon Vision Editor/SidebarViews.swift @@ -119,6 +119,14 @@ struct SidebarView: View { } return nil } + case "php": + toc = lines.enumerated().compactMap { index, line in + let t = line.trimmingCharacters(in: .whitespaces) + if t.hasPrefix("function ") || t.hasPrefix("class ") || t.hasPrefix("interface ") || t.hasPrefix("trait ") { + return "\(t) (Line \(index + 1))" + } + return nil + } case "objective-c": toc = lines.enumerated().compactMap { index, line in let t = line.trimmingCharacters(in: .whitespaces) @@ -154,7 +162,7 @@ struct SidebarView: View { } return nil } - case "html", "css", "json", "markdown": + case "html", "css", "json", "markdown", "csv": toc = lines.enumerated().compactMap { index, line in let trimmed = line.trimmingCharacters(in: .whitespaces) if !trimmed.isEmpty && (trimmed.hasPrefix("#") || trimmed.hasPrefix(" [String: C "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number, "//.*|/\\*([^*]|(\\*+[^*/]))*\\*+/": colors.comment ] + case "php": + return [ + #"\b(function|class|interface|trait|namespace|use|public|private|protected|static|final|abstract|if|else|elseif|for|foreach|while|do|switch|case|default|return|try|catch|throw|new|echo)\b"#: colors.keyword, + #"\$[A-Za-z_][A-Za-z0-9_]*|\$\{[^}]+\}"#: colors.variable, + #"\"[^\"]*\"|'[^']*'"#: colors.string, + #"\b([0-9]+(\.[0-9]+)?)\b"#: colors.number, + #"//.*|#.*|/\*([^*]|(\*+[^*/]))*\*+/"#: colors.comment, + #"<\?php|\?>"#: colors.meta + ] case "html": return ["<[^>]+>": colors.tag] case "css": @@ -136,10 +145,11 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: C ] case "json": return [ - "\"[^\"]+\"\\s*:": colors.property, - "\"[^\"]*\"": colors.string, - "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number, - "\\b(true|false|null)\\b": colors.keyword + #"\"[^\"]+\"\s*:"#: colors.property, + #"\"([^\"\\]|\\.)*\""#: colors.string, + #"\b(-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?)\b"#: colors.number, + #"\b(true|false|null)\b"#: colors.keyword, + #"[{}\[\],:]"#: colors.meta ] case "markdown": return [ @@ -258,9 +268,19 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: C ] case "toml": return [ - #"^\[[^\]]+\]"#: colors.meta, - #"\b[0-9]+\b"#: colors.number, - #"\"[^\"]*\""#: colors.string + #"^\s*\[\[?[^\]]+\]?\]\s*$"#: colors.meta, + #"^\s*[A-Za-z0-9_.-]+\s*="#: colors.property, + #"\"([^\"\\]|\\.)*\"|'[^']*'"#: colors.string, + #"\b(-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?)\b"#: colors.number, + #"\b(true|false)\b"#: colors.keyword, + #"(?m)#.*$"#: colors.comment + ] + case "csv": + return [ + #"\A([^\n,]+)(,\s*[^\n,]+)*"#: colors.meta, + #"\"([^\"\n]|\"\")*\""#: colors.string, + #"\b(-?[0-9]+(\.[0-9]+)?)\b"#: colors.number, + #","#: colors.property ] case "ini": return [ diff --git a/README.md b/README.md index a654927..b656ec2 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ No background indexing. No telemetry. No plugin sprawl. - Fast loading, including large text files - Automatic applied syntax highlighting for common languages - (Python, C/C++, JavaScript, HTML, CSS, and others) + (Python, PHP, C/C++, JavaScript, HTML, CSS, and others) - Clean, minimal UI optimized for readability - Native macOS 26 (Tahoe) look & behavior - Built with Swift and AppKit @@ -121,5 +121,15 @@ If you find Neon Vision Editor useful and want to support its development: git clone https://github.com/h3pdesign/Neon-Vision-Editor.git cd Neon-Vision-Editor open "Neon Vision Editor.xcodeproj" +``` + +## Homebrew install option + +If you use Homebrew, you can install via cask: + +```bash +brew tap h3pdesign/tap +brew install --cask neon-vision-editor +```