Skip to content
Merged
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
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions crates/wright-language/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 1 addition & 4 deletions crates/wright-language/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
169 changes: 13 additions & 156 deletions crates/wright-language/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,73 +17,6 @@ pub struct SourceDiagnostic {
pub document_version: i32,
}

#[derive(Debug, Clone, Serialize)]
pub struct Hover {
pub contents: String,
pub range: Option<Range>,
pub document_version: i32,
}

#[derive(Debug, Clone, Serialize)]
pub struct CompletionItem {
pub label: String,
pub kind: String,
pub detail: Option<String>,
}

#[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<RenameEdit>,
pub previews: Vec<wright_driver::edit::SourcePreview>,
pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceError {
pub code: String,
pub message: String,
pub span: Option<workshop_rs::source::Span>,
}

#[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<SemanticIndex>,
pub findings: Vec<Finding>,
pub parse_errors: Vec<SourceError>,
pub files: Vec<SourceFile>,
}

pub struct LanguageService {
pub store: DocumentStore,
pub root: PathBuf,
Expand All @@ -100,27 +30,22 @@ impl LanguageService {
}
}

pub fn analyze(&self, _document: &Document) -> Analysis {
unavailable_source_analysis()
}

pub fn diagnostics(&self, uri: &str) -> Vec<SourceDiagnostic> {
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<String> {
Expand All @@ -130,60 +55,6 @@ impl LanguageService {
Vec::new()
}
}

pub fn hover(&self, _uri: &str, _position: Position) -> Option<Hover> {
None
}

pub fn definition(&self, _uri: &str, _position: Position) -> Option<SourceLocation> {
None
}

pub fn references(&self, _uri: &str, _position: Position) -> Vec<SourceLocation> {
Vec::new()
}

pub fn completion(&self, _uri: &str, _position: Position) -> Vec<CompletionItem> {
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<SemanticToken> {
Vec::new()
}
}

fn refused_rename(document_version: i32, diagnostic: impl Into<String>) -> RenameResult {
RenameResult {
document_version,
ok: false,
edits: Vec::new(),
previews: Vec::new(),
diagnostics: vec![diagnostic.into()],
}
}

fn is_source_document(uri: &str) -> bool {
Expand All @@ -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 {
Expand Down
60 changes: 22 additions & 38 deletions crates/wright-language/tests/service.rs
Original file line number Diff line number Diff line change
@@ -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:"));
}
Loading
Loading