Add comprehensive PHP and CSV language support, refine JSON/TOML syntax highlighting, and document Homebrew install option

This commit is contained in:
h3p 2026-02-08 00:20:47 +01:00
parent 3eca225d8c
commit 8b0bc4a805
8 changed files with 120 additions and 18 deletions

View file

@ -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 "ObjectiveC"
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"

View file

@ -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("<?php") || lower.contains("<?=") || lower.contains("$this->") || 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"

View file

@ -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",

View file

@ -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("<html") || t.contains("<body") || t.contains("</") { bump("html", 90) }
if t.contains("<?php") || t.contains("<?=") { bump("php", 90) }
if raw.contains(",") && raw.contains("\n") {
let lines = raw.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 }) {
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

View file

@ -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)
}

View file

@ -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("<h")) {

View file

@ -122,6 +122,15 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [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 [

View file

@ -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
```