Skip to content

Commit 570cd5d

Browse files
committed
feat(lsp): integrate Java LSP into EditorView
- Add MonacoLSPWebView for Java files - EditorView automatically uses LSP version for Java - Connects to localhost:8080 LSP WebSocket - Shows LSP connection status
1 parent f042e95 commit 570cd5d

File tree

2 files changed

+219
-8
lines changed

2 files changed

+219
-8
lines changed

Sources/Zero/Views/EditorView.swift

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,31 @@ struct EditorView: View {
8484
Divider()
8585

8686
// 에디터
87-
CodeEditorView(
88-
content: $fileContent,
89-
language: currentLanguage,
90-
onReady: { isEditorReady = true },
91-
onCursorChange: { line, column in
92-
cursorLine = line
93-
cursorColumn = column
87+
Group {
88+
if currentLanguage == "java" {
89+
// Java LSP 버전
90+
MonacoLSPWebView(
91+
content: $fileContent,
92+
language: currentLanguage,
93+
onReady: { isEditorReady = true },
94+
onCursorChange: { line, column in
95+
cursorLine = line
96+
cursorColumn = column
97+
}
98+
)
99+
} else {
100+
// 기본 Monaco
101+
CodeEditorView(
102+
content: $fileContent,
103+
language: currentLanguage,
104+
onReady: { isEditorReady = true },
105+
onCursorChange: { line, column in
106+
cursorLine = line
107+
cursorColumn = column
108+
}
109+
)
94110
}
95-
)
111+
}
96112
.onChange(of: fileContent) { _, _ in
97113
if !isLoadingFile {
98114
hasUnsavedChanges = true
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import SwiftUI
2+
import WebKit
3+
4+
struct MonacoLSPWebView: NSViewRepresentable {
5+
@Binding var content: String
6+
var language: String
7+
var onReady: (() -> Void)?
8+
var onCursorChange: ((Int, Int) -> Void)?
9+
10+
func makeNSView(context: Context) -> WKWebView {
11+
let config = WKWebViewConfiguration()
12+
config.userContentController.add(context.coordinator, name: "editorReady")
13+
config.userContentController.add(context.coordinator, name: "contentChanged")
14+
config.userContentController.add(context.coordinator, name: "cursorChanged")
15+
16+
let webView = WKWebView(frame: .zero, configuration: config)
17+
webView.navigationDelegate = context.coordinator
18+
context.coordinator.webView = webView
19+
20+
// Load Monaco LSP HTML
21+
if let htmlPath = Bundle.main.path(forResource: "monaco-lsp", ofType: "html"),
22+
let htmlContent = try? String(contentsOfFile: htmlPath) {
23+
webView.loadHTMLString(htmlContent, baseURL: URL(string: "https://cdnjs.cloudflare.com"))
24+
} else {
25+
// Fallback to standard Monaco
26+
let html = Self.monacoLSPHTML
27+
webView.loadHTMLString(html, baseURL: URL(string: "https://cdnjs.cloudflare.com"))
28+
}
29+
30+
return webView
31+
}
32+
33+
func updateNSView(_ webView: WKWebView, context: Context) {
34+
// Update content when binding changes
35+
if context.coordinator.isReady {
36+
let escaped = content.replacingOccurrences(of: "\\", with: "\\\\")
37+
.replacingOccurrences(of: "'", with: "\\'")
38+
.replacingOccurrences(of: "\n", with: "\\n")
39+
webView.evaluateJavaScript("setContent('\(escaped)', '\(language)')")
40+
}
41+
}
42+
43+
func makeCoordinator() -> Coordinator {
44+
Coordinator(self)
45+
}
46+
47+
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
48+
var parent: MonacoLSPWebView
49+
var webView: WKWebView?
50+
var isReady = false
51+
52+
init(_ parent: MonacoLSPWebView) {
53+
self.parent = parent
54+
}
55+
56+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
57+
if message.name == "editorReady" {
58+
isReady = true
59+
parent.onReady?()
60+
61+
// Set initial content
62+
let escaped = parent.content.replacingOccurrences(of: "\\", with: "\\\\")
63+
.replacingOccurrences(of: "'", with: "\\'")
64+
.replacingOccurrences(of: "\n", with: "\\n")
65+
webView?.evaluateJavaScript("setContent('\(escaped)', '\(parent.language)')")
66+
67+
// Enable LSP after a delay
68+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
69+
self.webView?.evaluateJavaScript("enableLSP()")
70+
}
71+
} else if message.name == "contentChanged", let body = message.body as? String {
72+
parent.content = body
73+
} else if message.name == "cursorChanged", let body = message.body as? [String: Int] {
74+
if let line = body["line"], let column = body["column"] {
75+
parent.onCursorChange?(line, column)
76+
}
77+
}
78+
}
79+
}
80+
81+
// Embedded Monaco LSP HTML as fallback
82+
static let monacoLSPHTML = """
83+
<!DOCTYPE html>
84+
<html>
85+
<head>
86+
<meta charset="UTF-8">
87+
<style>
88+
* { margin: 0; padding: 0; box-sizing: border-box; }
89+
html, body { height: 100%; overflow: hidden; background: #1e1e1e; }
90+
#editor { width: 100%; height: 100%; }
91+
#status {
92+
position: fixed;
93+
bottom: 0;
94+
right: 0;
95+
padding: 4px 8px;
96+
background: #007acc;
97+
color: white;
98+
font-size: 12px;
99+
display: none;
100+
}
101+
#status.connected { display: block; background: #4caf50; }
102+
#status.disconnected { display: block; background: #f44336; }
103+
</style>
104+
</head>
105+
<body>
106+
<div id="editor"></div>
107+
<div id="status">LSP</div>
108+
109+
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
110+
<script>
111+
let editor;
112+
let lspSocket;
113+
let lspEnabled = false;
114+
let requestId = 0;
115+
const pendingRequests = new Map();
116+
117+
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
118+
require(['vs/editor/editor.main'], function () {
119+
editor = monaco.editor.create(document.getElementById('editor'), {
120+
value: '',
121+
language: 'java',
122+
theme: 'vs-dark',
123+
fontSize: 14,
124+
minimap: { enabled: true },
125+
automaticLayout: true,
126+
quickSuggestions: true,
127+
suggestOnTriggerCharacters: true
128+
});
129+
130+
window.webkit.messageHandlers.editorReady.postMessage('ready');
131+
132+
editor.onDidChangeModelContent(() => {
133+
window.webkit.messageHandlers.contentChanged.postMessage(editor.getValue());
134+
});
135+
136+
editor.onDidChangeCursorPosition((e) => {
137+
window.webkit.messageHandlers.cursorChanged.postMessage({
138+
line: e.position.lineNumber,
139+
column: e.position.column
140+
});
141+
});
142+
});
143+
144+
function connectLSP() {
145+
const statusEl = document.getElementById('status');
146+
statusEl.className = 'disconnected';
147+
statusEl.textContent = 'LSP Connecting...';
148+
149+
lspSocket = new WebSocket('ws://localhost:8080');
150+
151+
lspSocket.onopen = () => {
152+
console.log('LSP Connected');
153+
lspEnabled = true;
154+
statusEl.className = 'connected';
155+
statusEl.textContent = 'LSP Connected';
156+
157+
lspSocket.send(JSON.stringify({
158+
jsonrpc: '2.0',
159+
id: 1,
160+
method: 'initialize',
161+
params: {
162+
processId: null,
163+
rootUri: 'file:///workspace',
164+
capabilities: {}
165+
}
166+
}));
167+
};
168+
169+
lspSocket.onclose = () => {
170+
lspEnabled = false;
171+
statusEl.className = 'disconnected';
172+
statusEl.textContent = 'LSP Disconnected';
173+
};
174+
175+
lspSocket.onerror = () => {
176+
statusEl.className = 'disconnected';
177+
statusEl.textContent = 'LSP Error';
178+
};
179+
}
180+
181+
function setContent(content, language) {
182+
if (editor) {
183+
monaco.editor.setModelLanguage(editor.getModel(), language || 'java');
184+
editor.setValue(content);
185+
}
186+
}
187+
188+
function enableLSP() {
189+
connectLSP();
190+
}
191+
</script>
192+
</body>
193+
</html>
194+
"""
195+
}

0 commit comments

Comments
 (0)