From 86ca36ab291b55cb2c8fb80493e1776c0f782877 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:10:07 +0800 Subject: [PATCH 1/2] fix(lsp): advertise only backed wright-lsp capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wright-lsp advertised hover, definition, references, completion, rename, and semantic tokens while wright-language stubbed every one of them, and every document — including raw Workshop — received a source-provider-unavailable error. The initialize result now advertises only document synchronization; unadvertised requests return a uniform null result, source-language documents keep the explicit provider refusal, and other documents publish no diagnostics. The dead service stubs, unreachable adapter handlers, and unused wright-language dependencies are removed with the unbacked surface. Fixes #421 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- Cargo.lock | 3 - README.md | 6 + crates/wright-language/Cargo.toml | 7 +- crates/wright-language/src/lib.rs | 5 +- crates/wright-language/src/service.rs | 169 ++--------------- crates/wright-language/tests/service.rs | 60 +++--- crates/wright-lsp/src/main.rs | 237 +----------------------- crates/wright-lsp/tests/lsp.rs | 66 ++++++- docs/language-services.md | 116 ++++++------ docs/v1-matrix.md | 1 + 10 files changed, 160 insertions(+), 510 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23c3ab56..055d6fc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2161,12 +2161,9 @@ dependencies = [ name = "wright-language" version = "0.3.0" dependencies = [ - "libc", "serde", - "serde_json", "url", "workshop-rs", - "wright-analyzer", "wright-driver", ] diff --git a/README.md b/README.md index bc042023..8fbebdb8 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,12 @@ Workshop → OPY and Workshop → DEL reconstruction are not treated as supporte WrightKit capabilities until the owning language implementations provide and test those reconstruction paths. +`wright-lsp` currently advertises document synchronization and lifecycle only: +source-language documents publish an explicit `source-provider-unavailable` +diagnostic, and editor features such as hover, completion, or rename are not +advertised until a provider backs them. See +[`docs/language-services.md`](docs/language-services.md). + Compiled output converges structurally on the established upstream compiler (rule order, element identities, control flow, conditions, values, variable names and indices, and element cost), compared as canonical Workshop programs diff --git a/crates/wright-language/Cargo.toml b/crates/wright-language/Cargo.toml index 61c75c32..6001b082 100644 --- a/crates/wright-language/Cargo.toml +++ b/crates/wright-language/Cargo.toml @@ -5,19 +5,14 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Wright's editor-neutral language services: documents, diagnostics, hover, definition, references, completion, rename, semantic tokens." +description = "Wright's editor-neutral language services: documents and diagnostics." [lints] workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true url = "2" -wright-analyzer.workspace = true wright-driver.workspace = true # Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true - -[dev-dependencies] -libc.workspace = true diff --git a/crates/wright-language/src/lib.rs b/crates/wright-language/src/lib.rs index e87a7b14..06b00e05 100644 --- a/crates/wright-language/src/lib.rs +++ b/crates/wright-language/src/lib.rs @@ -2,7 +2,4 @@ pub mod document; pub mod service; pub use document::{Document, DocumentStore, Position, Range}; -pub use service::{ - CompletionItem, Hover, LanguageService, RenameEdit, SemanticToken, SourceDiagnostic, - SourceError, SourceFile, SourceLocation, -}; +pub use service::{LanguageService, SourceDiagnostic}; diff --git a/crates/wright-language/src/service.rs b/crates/wright-language/src/service.rs index f60ecb1a..db307f5c 100644 --- a/crates/wright-language/src/service.rs +++ b/crates/wright-language/src/service.rs @@ -3,11 +3,8 @@ use std::path::PathBuf; use serde::Serialize; -use workshop_rs::Program; -use wright_analyzer::analysis::Finding; -use wright_analyzer::canonical::SemanticIndex; -use crate::document::{Document, DocumentStore, Position, Range}; +use crate::document::{DocumentStore, Position, Range}; #[derive(Debug, Clone, Serialize)] pub struct SourceDiagnostic { @@ -20,73 +17,6 @@ pub struct SourceDiagnostic { pub document_version: i32, } -#[derive(Debug, Clone, Serialize)] -pub struct Hover { - pub contents: String, - pub range: Option, - pub document_version: i32, -} - -#[derive(Debug, Clone, Serialize)] -pub struct CompletionItem { - pub label: String, - pub kind: String, - pub detail: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SemanticToken { - pub line: u32, - pub character: u32, - pub length: u32, - pub token_type: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SourceLocation { - pub source: String, - pub range: Range, -} - -#[derive(Debug, Clone, Serialize)] -pub struct RenameEdit { - pub source: String, - pub range: Range, - pub new_text: String, - pub source_identity: String, - pub source_version: i32, -} - -#[derive(Debug, Clone, Serialize)] -pub struct RenameResult { - pub document_version: i32, - pub ok: bool, - pub edits: Vec, - pub previews: Vec, - pub diagnostics: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceError { - pub code: String, - pub message: String, - pub span: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceFile { - pub id: workshop_rs::source::FileId, - pub path: String, -} - -pub struct Analysis { - pub program: Program, - pub index: Option, - pub findings: Vec, - pub parse_errors: Vec, - pub files: Vec, -} - pub struct LanguageService { pub store: DocumentStore, pub root: PathBuf, @@ -100,27 +30,22 @@ impl LanguageService { } } - pub fn analyze(&self, _document: &Document) -> Analysis { - unavailable_source_analysis() - } - pub fn diagnostics(&self, uri: &str) -> Vec { let Some(document) = self.store.document(uri) else { return Vec::new(); }; - self.analyze(document) - .parse_errors - .into_iter() - .map(|error| SourceDiagnostic { - source: document.uri.clone(), - range: empty_range(), - severity: "error".to_string(), - code: error.code, - message: error.message, - source_version: document.version, - document_version: document.version, - }) - .collect() + if !is_source_document(uri) { + return Vec::new(); + } + vec![SourceDiagnostic { + source: document.uri.clone(), + range: empty_range(), + severity: "error".to_string(), + code: "source-provider-unavailable".to_string(), + message: "source-language analysis is provider-owned and no editor capability is currently negotiated".to_string(), + source_version: document.version, + document_version: document.version, + }] } pub fn dependent_documents(&self, uri: &str) -> Vec { @@ -130,60 +55,6 @@ impl LanguageService { Vec::new() } } - - pub fn hover(&self, _uri: &str, _position: Position) -> Option { - None - } - - pub fn definition(&self, _uri: &str, _position: Position) -> Option { - None - } - - pub fn references(&self, _uri: &str, _position: Position) -> Vec { - Vec::new() - } - - pub fn completion(&self, _uri: &str, _position: Position) -> Vec { - Vec::new() - } - - pub fn rename(&self, uri: &str, _position: Position, new_name: &str) -> RenameResult { - if new_name.is_empty() { - return refused_rename(0, "rename-invalid-name: the new name must not be empty"); - } - let Some(requesting) = self.store.document(uri) else { - return refused_rename( - 0, - format!( - "rename-unresolved: no open document for '{uri}'; the source identity cannot be established" - ), - ); - }; - if is_source_document(uri) { - return refused_rename( - requesting.version, - "source-provider-unavailable: OPY language-service capabilities are not currently shipped with the configured provider", - ); - } - - refused_rename( - requesting.version, - "rename-unresolved: no symbol is resolvable at the requested position", - ) - } - pub fn semantic_tokens(&self, _uri: &str) -> Vec { - Vec::new() - } -} - -fn refused_rename(document_version: i32, diagnostic: impl Into) -> RenameResult { - RenameResult { - document_version, - ok: false, - edits: Vec::new(), - previews: Vec::new(), - diagnostics: vec![diagnostic.into()], - } } fn is_source_document(uri: &str) -> bool { @@ -192,20 +63,6 @@ fn is_source_document(uri: &str) -> bool { .is_some_and(|ext| matches!(ext.as_str(), "opy" | "ostw" | "del")) } -fn unavailable_source_analysis() -> Analysis { - Analysis { - program: Program::default(), - index: None, - findings: Vec::new(), - parse_errors: vec![SourceError { - code: "source-provider-unavailable".to_string(), - message: "source-language analysis is provider-owned and no editor capability is currently negotiated".to_string(), - span: None, - }], - files: Vec::new(), - } -} - fn empty_range() -> Range { Range { start: Position { diff --git a/crates/wright-language/tests/service.rs b/crates/wright-language/tests/service.rs index 84848841..58b9949c 100644 --- a/crates/wright-language/tests/service.rs +++ b/crates/wright-language/tests/service.rs @@ -1,57 +1,41 @@ use std::path::PathBuf; use wright_language::LanguageService; -use wright_language::document::{Document, Position}; +use wright_language::document::Document; fn service() -> LanguageService { LanguageService::new(PathBuf::from(".")) } #[test] -fn opy_language_service_reports_provider_boundary_without_static_fallback() { +fn source_documents_report_provider_boundary_without_static_fallback() { let mut service = service(); - let document = Document::new( - "file:///workspace/main.opy".to_string(), - "globalvar score = 0\n".to_string(), - PathBuf::from("."), - ); - service.store.open(document); - let diagnostics = service.diagnostics("file:///workspace/main.opy"); - assert_eq!(diagnostics[0].code, "source-provider-unavailable"); - assert!( - service - .completion( - "file:///workspace/main.opy", - Position { - line: 0, - character: 0 - } - ) - .is_empty() - ); - assert!( - service - .semantic_tokens("file:///workspace/main.opy") - .is_empty() - ); + for ext in ["opy", "ostw", "del"] { + let uri = format!("file:///workspace/main.{ext}"); + service.store.open(Document::new( + uri.clone(), + "globalvar score = 0\n".to_string(), + PathBuf::from("."), + )); + let diagnostics = service.diagnostics(&uri); + assert_eq!( + diagnostics[0].code, "source-provider-unavailable", + "{ext} documents surface an explicit provider refusal" + ); + } } #[test] -fn opy_rename_refuses_without_provider_editor_capability() { +fn workshop_documents_publish_no_provider_diagnostics() { let mut service = service(); + let uri = "file:///workspace/main.ws"; service.store.open(Document::new( - "file:///workspace/main.opy".to_string(), - "globalvar score = 0\n".to_string(), + uri.to_string(), + "rule(\"demo\") {\n event {\n Ongoing - Global;\n }\n}\n".to_string(), PathBuf::from("."), )); - let result = service.rename( - "file:///workspace/main.opy", - Position { - line: 0, - character: 10, - }, - "total", + assert!( + service.diagnostics(uri).is_empty(), + "a raw Workshop document does not require a source provider" ); - assert!(!result.ok); - assert!(result.diagnostics[0].starts_with("source-provider-unavailable:")); } diff --git a/crates/wright-lsp/src/main.rs b/crates/wright-lsp/src/main.rs index 5eb1c420..a0d863f7 100644 --- a/crates/wright-lsp/src/main.rs +++ b/crates/wright-lsp/src/main.rs @@ -7,21 +7,16 @@ use std::str::FromStr; use lsp_types::notification::{Notification, PublishDiagnostics}; use lsp_types::{ - CompletionItem as LspCompletionItem, CompletionItemKind, CompletionParams, Diagnostic as LspDiagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, - DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentChanges, GotoDefinitionParams, - GotoDefinitionResponse, Hover as LspHover, HoverContents, InitializeParams, InitializeResult, - Location, MarkupContent, MarkupKind, OptionalVersionedTextDocumentIdentifier, + DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, InitializeResult, Position as LspPosition, PositionEncodingKind, PublishDiagnosticsParams, Range as LspRange, - ReferenceParams, RenameParams, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, - SemanticTokensParams, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, - TextDocumentEdit, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, TextEdit, Uri, WorkDoneProgressOptions, WorkspaceEdit, + ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, Uri, }; use serde_json::Value; -use wright_language::document::{Document, Position, Range}; -use wright_language::{LanguageService, SemanticToken as WtToken}; +use wright_language::LanguageService; +use wright_language::document::{Document, Range}; type PublicationOwnership = BTreeMap>; @@ -115,130 +110,6 @@ fn run() -> Result<(), String> { publish_affected_diagnostics(&mut writer, &service, &mut ownership, &uri)?; } "textDocument/didSave" => {} - "textDocument/hover" => { - let params: TextDocumentPositionParams = parse_params(params)?; - let position = convert_position(params.position); - let result = service - .hover(¶ms.text_document.uri.to_string(), position) - .map(|hover| LspHover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: hover.contents, - }), - range: hover.range.map(convert_range), - }); - write_response(&mut writer, id, serde_json::to_value(result).unwrap())?; - } - "textDocument/definition" => { - let params: GotoDefinitionParams = parse_params(params)?; - let position = convert_position(params.text_document_position_params.position); - let result = service - .definition( - ¶ms - .text_document_position_params - .text_document - .uri - .to_string(), - position, - ) - .map(|loc| { - GotoDefinitionResponse::Scalar(Location { - uri: source_to_uri(&loc.source), - range: convert_range(loc.range), - }) - }); - write_response(&mut writer, id, serde_json::to_value(result).unwrap())?; - } - "textDocument/references" => { - let params: ReferenceParams = parse_params(params)?; - let position = convert_position(params.text_document_position.position); - let uri = params.text_document_position.text_document.uri; - let result: Vec = service - .references(&uri.to_string(), position) - .into_iter() - .map(|loc| Location { - uri: source_to_uri(&loc.source), - range: convert_range(loc.range), - }) - .collect(); - write_response(&mut writer, id, serde_json::to_value(result).unwrap())?; - } - "textDocument/completion" => { - let params: CompletionParams = parse_params(params)?; - let position = convert_position(params.text_document_position.position); - let uri = params.text_document_position.text_document.uri; - let items: Vec = service - .completion(&uri.to_string(), position) - .into_iter() - .map(|item| LspCompletionItem { - label: item.label, - kind: Some(completion_kind(&item.kind)), - detail: item.detail, - ..Default::default() - }) - .collect(); - write_response(&mut writer, id, serde_json::to_value(items).unwrap())?; - } - "textDocument/rename" => { - let params: RenameParams = parse_params(params)?; - let position = convert_position(params.text_document_position.position); - let uri = params.text_document_position.text_document.uri.to_string(); - let rename = service.rename(&uri, position, ¶ms.new_name); - if rename.ok { - let mut by_source: BTreeMap> = - BTreeMap::new(); - for edit in rename.edits { - by_source.entry(edit.source.clone()).or_default().push(edit); - } - let document_changes = DocumentChanges::Edits( - by_source - .into_iter() - .map(|(source, edits)| TextDocumentEdit { - text_document: OptionalVersionedTextDocumentIdentifier { - uri: source_to_uri(&source), - version: source_version(&service, &source), - }, - edits: edits - .into_iter() - .map(|edit| { - lsp_types::OneOf::Left(TextEdit { - range: convert_range(edit.range), - new_text: edit.new_text, - }) - }) - .collect(), - }) - .collect(), - ); - let workspace_edit = WorkspaceEdit { - document_changes: Some(document_changes), - ..Default::default() - }; - write_response( - &mut writer, - id, - serde_json::to_value(workspace_edit).unwrap(), - )?; - } else { - let detail = rename.diagnostics.join("; "); - write_error( - &mut writer, - id, - -32602, - &format!("rename refused: {detail}"), - )?; - } - } - "textDocument/semanticTokens/full" => { - let params: SemanticTokensParams = parse_params(params)?; - let uri = params.text_document.uri; - let tokens = service.semantic_tokens(&uri.to_string()); - let result = SemanticTokens { - result_id: None, - data: encode_semantic_tokens(&tokens), - }; - write_response(&mut writer, id, serde_json::to_value(result).unwrap())?; - } _ => { if id.is_some() { write_response(&mut writer, id, Value::Null)?; @@ -260,39 +131,6 @@ fn initialize_result() -> InitializeResult { ..Default::default() }, )), - hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), - definition_provider: Some(lsp_types::OneOf::Left(true)), - references_provider: Some(lsp_types::OneOf::Left(true)), - completion_provider: Some(lsp_types::CompletionOptions { - trigger_characters: None, - resolve_provider: Some(false), - ..Default::default() - }), - rename_provider: Some(lsp_types::OneOf::Left(true)), - semantic_tokens_provider: Some( - SemanticTokensServerCapabilities::SemanticTokensOptions( - lsp_types::SemanticTokensOptions { - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: None, - }, - legend: SemanticTokensLegend { - token_types: vec![ - "keyword".into(), - "variable".into(), - "identifier".into(), - "string".into(), - "number".into(), - "operator".into(), - "macro".into(), - "attribute".into(), - ], - token_modifiers: vec![], - }, - range: Some(false), - full: Some(SemanticTokensFullOptions::Bool(true)), - }, - ), - ), ..Default::default() }, server_info: Some(ServerInfo { @@ -341,18 +179,6 @@ fn parse_params(params: Option) -> Result serde_json::from_value(params.unwrap()).map_err(|error| error.to_string()) } -fn write_error( - writer: &mut impl Write, - id: Option, - code: i64, - message: &str, -) -> Result<(), String> { - write_msg( - writer, - serde_json::json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }), - ) -} - fn publish_affected_diagnostics( writer: &mut impl Write, service: &LanguageService, @@ -456,13 +282,6 @@ fn source_version(service: &LanguageService, source: &str) -> Option { .map(|d| d.version) } -fn convert_position(position: LspPosition) -> Position { - Position { - line: position.line, - character: position.character, - } -} - #[allow(deprecated)] fn initialize_root(params: &InitializeParams) -> Option { if let Some(uri) = ¶ms.root_uri { @@ -505,49 +324,3 @@ fn convert_range(range: Range) -> LspRange { }, } } - -fn completion_kind(kind: &str) -> CompletionItemKind { - match kind { - "keyword" => CompletionItemKind::KEYWORD, - "globalVariable" | "playerVariable" | "variable" => CompletionItemKind::VARIABLE, - "subroutine" | "function" => CompletionItemKind::FUNCTION, - "rule" => CompletionItemKind::CLASS, - _ => CompletionItemKind::TEXT, - } -} - -fn encode_semantic_tokens(tokens: &[WtToken]) -> Vec { - let mut encoded = Vec::new(); - let mut prev_line = 0u32; - let mut prev_char = 0u32; - - for token in tokens { - let delta_line = token.line.saturating_sub(prev_line); - let delta_start = if delta_line == 0 { - token.character.saturating_sub(prev_char) - } else { - token.character - }; - let token_type_index = match token.token_type.as_str() { - "keyword" => 0, - "variable" => 1, - "identifier" => 2, - "string" => 3, - "number" => 4, - "operator" => 5, - "macro" => 6, - "attribute" => 7, - _ => 2, - }; - encoded.push(lsp_types::SemanticToken { - delta_line, - delta_start, - length: token.length, - token_type: token_type_index, - token_modifiers_bitset: 0, - }); - prev_line = token.line; - prev_char = token.character; - } - encoded -} diff --git a/crates/wright-lsp/tests/lsp.rs b/crates/wright-lsp/tests/lsp.rs index d8aab8ef..960fcdd5 100644 --- a/crates/wright-lsp/tests/lsp.rs +++ b/crates/wright-lsp/tests/lsp.rs @@ -1,8 +1,9 @@ -//! LSP contract tests after the OPY provider cutover. +//! LSP contract tests for the currently backed capability set. //! -//! Raw Workshop remains an in-process LSP workflow. OPY language-service -//! behavior is provider-owned and must surface an explicit refusal instead of -//! selecting a removed native frontend. +//! `wright-lsp` advertises document synchronization only; unbacked editor +//! capabilities are neither advertised nor answered. OPY/DEL/OSTW documents +//! report an explicit `source-provider-unavailable` diagnostic, while raw +//! Workshop documents receive no diagnostic at all. use std::io::{BufRead, BufReader, Read, Write}; use std::path::{Path, PathBuf}; @@ -124,12 +125,37 @@ fn lsp_binary_reports_the_implementation_version_non_interactively() { } #[test] -fn workshop_lsp_workflow_keeps_protocol_and_lifecycle_contracts() { +fn initialize_advertises_only_backed_capabilities() { let root = workspace_root(); let mut client = LspClient::spawn(&root); let init = initialize(&mut client); assert_eq!(init["result"]["serverInfo"]["name"], "wright-lsp"); - assert!(init["result"]["capabilities"]["hoverProvider"] == true); + let capabilities = init["result"]["capabilities"].as_object().unwrap(); + assert!(capabilities.contains_key("textDocumentSync")); + for capability in [ + "hoverProvider", + "definitionProvider", + "referencesProvider", + "completionProvider", + "renameProvider", + "semanticTokensProvider", + ] { + assert!( + !capabilities.contains_key(capability), + "unbacked capability must not be advertised: {capability}" + ); + } + client.notify("initialized", serde_json::json!({})); + let shutdown = client.request(2, "shutdown", serde_json::json!(null)); + assert!(shutdown["result"].is_null()); + client.notify("exit", serde_json::json!(null)); +} + +#[test] +fn workshop_lsp_workflow_keeps_protocol_and_lifecycle_contracts() { + let root = workspace_root(); + let mut client = LspClient::spawn(&root); + initialize(&mut client); client.notify("initialized", serde_json::json!({})); let source = "rule(\"demo\") {\n event {\n Ongoing - Global;\n }\n}\n"; @@ -146,9 +172,28 @@ fn workshop_lsp_workflow_keeps_protocol_and_lifecycle_contracts() { ); let published = client.read_notification("textDocument/publishDiagnostics"); assert_eq!(published["params"]["version"], 1); - assert!(published["params"]["diagnostics"].is_array()); + assert!( + published["params"]["diagnostics"] + .as_array() + .unwrap() + .is_empty(), + "a valid raw Workshop document publishes no diagnostics" + ); - let shutdown = client.request(2, "shutdown", serde_json::json!(null)); + let hover = client.request( + 2, + "textDocument/hover", + serde_json::json!({ + "textDocument": { "uri": "file:///workspace/main.ws" }, + "position": { "line": 0, "character": 1 }, + }), + ); + assert!( + hover["result"].is_null() && hover.get("error").is_none(), + "an unadvertised request returns a null result: {hover}" + ); + + let shutdown = client.request(3, "shutdown", serde_json::json!(null)); assert!(shutdown["result"].is_null()); client.notify("exit", serde_json::json!(null)); } @@ -182,7 +227,10 @@ fn opy_lsp_workflow_reports_provider_boundary_without_static_fallback() { "position": { "line": 0, "character": 0 }, }), ); - assert!(completion["result"].as_array().unwrap().is_empty()); + assert!( + completion["result"].is_null() && completion.get("error").is_none(), + "an unadvertised request returns a null result: {completion}" + ); let shutdown = client.request(3, "shutdown", serde_json::json!(null)); assert!(shutdown["result"].is_null()); diff --git a/docs/language-services.md b/docs/language-services.md index eaf242a7..a1c3a7a3 100644 --- a/docs/language-services.md +++ b/docs/language-services.md @@ -1,6 +1,6 @@ # Wright Language Services and LSP -Status: accepted baseline (living language services and LSP contract) +Status: current scope — document synchronization and diagnostics only Scope: editor-neutral language services (`wright-language`) and the thin LSP adapter (`wright-lsp`) @@ -9,11 +9,7 @@ adapter (`wright-lsp`) ```text document/workspace model (Document, DocumentStore) → LanguageService (editor-neutral, no LSP types) - ├─ diagnostics (provider capability or explicit refusal) - ├─ hover / definition / references (provider capability) - ├─ completion (provider capability) - ├─ rename (provider capability) - └─ semantic tokens (provider capability) + └─ diagnostics (provider refusal for source documents) ↓ wright-lsp (thin protocol adapter, Content-Length stdio framing) ``` @@ -22,6 +18,38 @@ All semantic logic lives in `wright-language`; `wright-lsp` only maps LSP DTOs, so adding or removing an editor protocol never changes the compiler or analyzer contracts. +## Current LSP scope + +`wright-lsp` backs and advertises only what is implemented: + +- lifecycle: `initialize`, `initialized`, `shutdown`, `exit`; +- full-document synchronization: `textDocument/didOpen`, + `textDocument/didChange`, `textDocument/didClose` (`didSave` is an explicit + no-op under full sync); +- `textDocument/publishDiagnostics`: versioned, grouped by source identity, + with didClose cleanup and per-root publication ownership — a source that + disappears from a root analysis is retired with an empty + publishDiagnostics unless another open root still owns it, and affected + documents are republished on include/overlay changes. + +The `initialize` result advertises `textDocumentSync` and the UTF-16 position +encoding only. It contains no provider entry for hover, definition, +references, completion, rename, or semantic tokens: those capabilities have +no backing implementation and are never advertised. A request for an +unadvertised or unknown method receives a `result: null` response and the +server keeps running. + +Diagnostics: opening a source-language document (`.opy`, `.del`, `.ostw`) +publishes an explicit `source-provider-unavailable` error while no provider +language-service capability is negotiated. A raw Workshop document (or any +document without a source language) publishes no diagnostics. + +Provider-backed editor capabilities — hover, definition, references, +completion, rename, and semantic tokens — are future work tracked under #156 +and the owning implementations (for example `opy-rs` language-service +capabilities). They arrive through provider capability negotiation, not +through Wright-side reimplementation or static fallbacks. + ## Editor-neutral contracts * `Document`: URI identity, current text, monotonic internal `version`, @@ -30,7 +58,7 @@ analyzer contracts. * Positions and ranges are 0-based editor conventions; the service converts to the compiler's 1-based spans at the boundary. UTF-16 ↔ character conversion is centralized in `wright_language::document` (`utf16_offset_to_char`, - `char_offset_to_utf16`, `span_to_range`, `full_document_range`) and is the + `char_offset_to_utf16`, `span_to_range`) and is the only path that consumes or emits editor positions; no UTF-16 offset is ever used as a byte index. * File URI ↔ filesystem path conversion is centralized in @@ -60,8 +88,8 @@ Re-evaluation trigger (measured, testable): re-open the true-cancellation or incremental-analysis decision when any of the following holds on the current machine baseline: -- the committed perf harness mean for `analyze` or `diagnostics` exceeds the - harness regression bound (200 ms per workflow); or +- a measured perf workload mean for `diagnostics` exceeds the recorded + regression bound (200 ms per workflow); or - peak RSS on the perf workload exceeds 1 GB; or - a representative project-scale measurement (a multi-file corpus of at least 20 source files, or the declared representative project) shows any single @@ -70,39 +98,20 @@ machine baseline: Until a trigger fires, bounded full recomputation with stale-result suppression is the authoritative contract. -## Services - -* **Diagnostics**: source-aware `SourceDiagnostic`s, including explicit - `source-provider-unavailable` when no provider editor capability is - negotiated. -* **Hover / Definition / References / Completion**: provider-owned; Wright - does not duplicate OPY parsing, manifests, or semantic indexes. -* **Rename**: project-wide identifier-exact rename: resolves the symbol - through the semantic index, unions its exact declaration/definition/reference - identifier spans across every open root whose project includes the - requesting document, and returns source-aware full-document edits for all - affected sources (open overlays take precedence over filesystem content). - Collisions, unresolvable identity, a missing exact identifier span, stale - source identity, and failed validation refuse explicitly. Edits are - Wright-owned (`RenameEdit`/`TargetSpan` in `wright-language`) and carry the - SHA-256 source identity computed through `wright_driver::input_identity`. - Rename delegates target resolution, edit generation, and validation to the - shared driver refactoring contract - (`wright_driver::edit::semantic_rename`, #129): every affected root - resolves through its original owner-backed source implementation, the unioned exact-range - transaction is validated through the shared #128 transaction boundary - (`wright_driver::edit::validate_transaction`), and no duplicate - edit-validation or span-collection semantics live here. -* **Semantic tokens**: provider-owned; no static lexer fallback is shipped. +## Diagnostics + +The service produces source-aware `SourceDiagnostic`s. Today the only +diagnostic is the explicit `source-provider-unavailable` refusal for +source-language documents, which marks where a negotiated provider +capability would attach; it is never a static fallback result. ## DEL/OSTW documents (#120) `.ostw`/`.del` documents remain recognized entrypoints for a future source provider, but Wright no longer carries a static DEL/OSTW implementation. The -service reports `source-provider-unavailable` as a structured source error and -does not produce semantic tokens, navigation, or rename edits for these -documents. It never invokes an upstream compiler or a removed Wright -implementation as a fallback. +service reports `source-provider-unavailable` as a structured source error +for these documents. It never invokes an upstream compiler or a removed +Wright implementation as a fallback. Workshop → OPY reconstruction is available only when the provider negotiates `reconstruct`; Workshop → OSTW is refused at the provider boundary until a @@ -110,32 +119,15 @@ provider is configured. ## LSP adapter -`wright-lsp` (stdio, Content-Length framing) implements: initialize -(capability negotiation: hover/definition/references/completion/rename/full -semantic tokens), didOpen/didChange/didSave/didClose (didSave is an explicit -no-op for full-sync documents; didClose retires diagnostics), publishDiagnostics -(versioned and grouped by source identity, with didClose cleanup), per-root -publication ownership: a source that disappears from a root analysis is -retired with an empty publishDiagnostics unless another open root still owns -it, so no diagnostic stays stale solely because its source left the analysis, -dependency-refresh of affected documents on include/overlay changes, hover, -definition, references, completion, rename (multi-document workspace edit), -semanticTokens/full, shutdown/exit. - -**Rename is a pure adapter (#131)**: the LSP layer maps the shared #129 -transaction (exact-occurrence editor edits from `wright-language`) directly -to `WorkspaceEdit` `documentChanges`/`TextDocumentEdit`, creating one `TextEdit` per -semantic occurrence, grouped by document, with each open document identified -at its current version and filesystem-backed sources in the unversioned -`null` form. No symbol resolution, collision, or stale-state logic exists in -the protocol layer; unsupported rename targets surface the shared refusal as -an explicit LSP error, never a textual fallback. The end-to-end - harness (`wright-lsp/tests/lsp.rs`) drives the real binary and verifies - capability negotiation, lifecycle, refusal routing, and stale-version - suppression. +`wright-lsp` (stdio, Content-Length framing) implements the scope listed +under *Current LSP scope* above and nothing else. The end-to-end harness +(`wright-lsp/tests/lsp.rs`) drives the real binary and verifies capability +negotiation, lifecycle, the diagnostics boundary, and null responses to +unadvertised requests. ## Out of scope (recorded) VS Code/browser extensions, incremental diff-based reanalysis (full -recomputation is deterministic and fast enough), and client-side behavior -scenarios remain future work. +recomputation is deterministic and fast enough), client-side behavior +scenarios, and provider-backed editor features until a provider capability +exists remain future work. diff --git a/docs/v1-matrix.md b/docs/v1-matrix.md index 41bb0130..d1e206dc 100644 --- a/docs/v1-matrix.md +++ b/docs/v1-matrix.md @@ -8,6 +8,7 @@ Status: current provider-boundary baseline | DEL / OSTW source workflows | `deltin-rs` or a future provider | Explicit `source-provider-unavailable` diagnostics | | Canonical Workshop text and WIR | `workshop-rs` | `wright-analyzer`, `wright-transform`, and embedding tests | | Driver and CLI result contracts | Wright | `wright-driver` and `wright-cli` tests | +| Language services / LSP | Wright | `wright-lsp` advertises document synchronization only; source-language documents report `source-provider-unavailable` | Wright does not claim OPY or DEL/OSTW language completeness from a successful build. Provider capabilities and source-language semantics remain bounded by From 50356dc138e119763d509aaccd09835d87acb541 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:50:51 +0800 Subject: [PATCH 2/2] docs(lsp): drop unbacked diagnostics republication claims Refs #421 --- docs/language-services.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/language-services.md b/docs/language-services.md index a1c3a7a3..35a09f41 100644 --- a/docs/language-services.md +++ b/docs/language-services.md @@ -27,10 +27,7 @@ analyzer contracts. `textDocument/didChange`, `textDocument/didClose` (`didSave` is an explicit no-op under full sync); - `textDocument/publishDiagnostics`: versioned, grouped by source identity, - with didClose cleanup and per-root publication ownership — a source that - disappears from a root analysis is retired with an empty - publishDiagnostics unless another open root still owns it, and affected - documents are republished on include/overlay changes. + with didClose cleanup. The `initialize` result advertises `textDocumentSync` and the UTF-16 position encoding only. It contains no provider entry for hover, definition,