Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ rust/target/
.idea/
DerivedData/
/windows/build*/
windows/tauri/src-tauri/target/
windows/tauri/src-tauri/gen/
windows/tauri/dist/
dist/
.artifacts/
*.pdb
*.ilk
*.exp
Fixtures/**/target/
*.xcuserstate
*.xcuserdata/
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/language-tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ LSP 控制中心标题栏的工具设置会在用户偏好中保存每个 provid

当前 transport 是 LSP 标准的 stdio `Content-Length` framing。一个 provider 在一个 workspace root 下复用一个 session;同一 provider 切换到另一个 root 时,manager 会停止旧 session 并创建新 session。

生产路径由 Rust engine 持有长生命周期 `sessionID -> RuntimeSession` registry。每个 runtime 同时拥有子进程、stdio、frame buffer、文档版本、pending request/deadline、capability 和 diagnostics;Swift 只保存不透明 `sessionID` 与 application-level `operationID`。`syncDocument` 由 Rust 决定发送 version 1 的 `didOpen` 或递增版本的 `didChange``pollEvents` 只返回 typed state/feature/diagnostic/result/error 事件。协议 reducer/host 只作为 engine 内部实现与纯函数测试 seam,不属于应用公开命令面。
生产路径由 Rust engine 持有长生命周期 `sessionID -> RuntimeSession` registry。每个 runtime 同时拥有子进程、stdio、frame buffer、文档版本、pending request/deadline、capability 和 diagnostics;Swift 只保存不透明 `sessionID` 与 application-level `operationID`。`syncDocument` 由 Rust 决定发送 version 1 的 `didOpen` 或递增版本的 `didChange`;当 server 声明 Incremental `textDocumentSync` 且请求携带 range 时发送 range-based `didChange`,否则发送全文。`pollEvents` 立即排空队列;`waitEvents` 在 session 事件 channel 上等待直到有事件或超时后再排空 typed state/feature/diagnostic/result/error 事件。协议 reducer/host 只作为 engine 内部实现与纯函数测试 seam,不属于应用公开命令面。

启动顺序:

Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/lsp-runtime-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ Primary files:
cross-platform process abstraction.
- Move stdin/stdout/stderr, partial-frame buffering, and malformed-frame
failure handling into the Rust session.
- Add an event queue drained through `lsp.pollEvents`.
- Add an event queue drained through `lsp.pollEvents` or waited on through
`lsp.waitEvents`.
- Implement initialize, request, and shutdown deadlines; fail every pending
request exactly once on timeout, cancellation, crash, stop, or restart.

Expand Down
99 changes: 94 additions & 5 deletions rust/lithe-core/src/lsp/interface/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! Pure LSP client-state transitions and JSON-RPC message construction.

use super::types::*;
use crate::lsp::{
apply_content_changes_sequential, order_incremental_changes_for_replay, ApplyTextEditsRequest,
LspTextEdit,
};
use crate::protocol::{CoreError, ErrorCode};
use serde_json::{json, Value};

Expand Down Expand Up @@ -147,29 +151,97 @@ pub fn client_change_document(
) -> Result<LspClientResponse, CoreError> {
validate_uri(&request.uri)?;
let mut state = request.state;
let Some(document) = state.open_documents.get_mut(&request.uri) else {
let Some(document) = state.open_documents.get(&request.uri).cloned() else {
return Err(CoreError::new(
ErrorCode::InvalidRequest,
"Cannot change a document that is not open in the LSP client.",
));
};
let wants_incremental = state.text_document_sync == LspTextDocumentSyncKind::Incremental
&& !request.content_changes.is_empty();
let ordered_incremental = if wants_incremental {
order_incremental_changes_for_replay(&document.text, &request.content_changes)?
} else {
None
};
let (next_text, wire_incremental) = if let Some(changes) = ordered_incremental {
(
apply_content_changes_sequential(&document.text, &changes)?,
Some(changes),
)
} else if !request.text.is_empty() {
// Host-provided full text, or full-sync fallback after an unsafe batch.
(request.text, None)
} else if !request.content_changes.is_empty() {
// No full text available; derive local document from the ranged batch.
(
apply_content_changes_as_simultaneous_edits(&document.text, &request.content_changes)?,
None,
)
} else {
(String::new(), None)
};
let content_changes = if let Some(changes) = wire_incremental.as_ref() {
json!(changes
.iter()
.map(|change| {
let range = change.range.expect("incremental changes require a range");
json!({
"range": {
"start": {
"line": range.start.line,
"character": range.start.utf16_column
},
"end": {
"line": range.end.line,
"character": range.end.utf16_column
}
},
"text": change.text
})
})
.collect::<Vec<_>>())
} else {
json!([{ "text": next_text }])
};
let mut document = document;
document.version += 1;
document.text = request.text;
document.text = next_text;
let message = json_rpc_notification(
"textDocument/didChange",
json!({
"textDocument": {
"uri": document.uri,
"version": document.version
},
"contentChanges": [{
"text": document.text
}]
"contentChanges": content_changes
}),
)?;
state.open_documents.insert(request.uri, document);
Ok(client_response(state, vec![message], Vec::new()))
}

fn apply_content_changes_as_simultaneous_edits(
text: &str,
changes: &[LspDocumentContentChange],
) -> Result<String, CoreError> {
let mut edits = Vec::new();
for change in changes {
let Some(range) = change.range else {
return Ok(change.text.clone());
};
edits.push(LspTextEdit {
range,
new_text: change.text.clone(),
});
}
Ok(crate::lsp::apply_text_edits(ApplyTextEditsRequest {
text: text.to_string(),
edits,
})?
.text)
}

/// Closes a document and clears diagnostics owned by its URI.
pub fn client_close_document(
request: ClientCloseDocumentRequest,
Expand Down Expand Up @@ -352,6 +424,8 @@ pub fn client_apply_server_message(
state.server_capabilities = feature_names_from_capabilities(
result.get("capabilities").unwrap_or(&Value::Null),
);
state.text_document_sync =
text_document_sync_kind(result.get("capabilities").unwrap_or(&Value::Null));
state.initialized = true;
responses.push(json_rpc_notification("initialized", json!({}))?);
}
Expand All @@ -360,6 +434,7 @@ pub fn client_apply_server_message(
state.initialized = false;
state.shutdown_requested = false;
state.server_capabilities.clear();
state.text_document_sync = LspTextDocumentSyncKind::Full;
state.open_documents.clear();
state.diagnostics.clear();
state.diagnostic_versions.clear();
Expand Down Expand Up @@ -1159,6 +1234,20 @@ fn hex_value(value: u8) -> Option<u8> {
}
}

fn text_document_sync_kind(capabilities: &Value) -> LspTextDocumentSyncKind {
let sync = capabilities.get("textDocumentSync");
let change = match sync {
Some(Value::Number(value)) => value.as_i64().unwrap_or(1),
Some(Value::Object(value)) => value.get("change").and_then(Value::as_i64).unwrap_or(1),
_ => 1,
};
match change {
0 => LspTextDocumentSyncKind::None,
2 => LspTextDocumentSyncKind::Incremental,
_ => LspTextDocumentSyncKind::Full,
}
}

fn feature_names_from_capabilities(capabilities: &Value) -> Vec<String> {
let mut values = Vec::new();
add_capability(
Expand Down
Loading
Loading