diff --git a/Cargo.toml b/Cargo.toml index 018da7e..2d3d6d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,5 +50,7 @@ subprocess = "=0.2.9" thiserror = "2.0" urlencoding = "2.1" utf8-read = "0.4" +scip = "0.6.1" +protobuf = "3" walkdir = "2" heck = "0.5" diff --git a/USAGE.md b/USAGE.md index ab1d23a..e1cb6d9 100644 --- a/USAGE.md +++ b/USAGE.md @@ -117,3 +117,80 @@ method now_we_can_declare_this_method_with_a_really_really_really_really_long na ``` Will allow 'long_lines' globally, 'nsp_unary' and 'indent_no_tabs' on the `param p = (1 ++ *` line, and 'indent_paren_expr' on the `'4);` line. + +## SCIP Export + +The DLS can export a [SCIP index](https://sourcegraph.com/docs/code-search/code-navigation/scip) +of analyzed DML devices. SCIP (Source Code Intelligence Protocol) is a +language-agnostic format for code intelligence data, used by tools such as +Sourcegraph for cross-repository navigation and code search. + +### Invocation + +SCIP export is available through the DFA (DML File Analyzer) binary via the +`--scip-output ` flag: +``` +dfa --compile-info --workspace --scip-output [list of devices to analyze, ] +``` + +It is worth noting that SCIP format specifies that symbols from documents that are not under the project root (which we define as the workspace) get slotted under external symbols with no occurances tracked. + +### SCIP schema details +Here we list how we have mapped DML specifically to the SCIP format. + +#### SCIP symbol kind mappings + +DML symbol kinds are mapped to SCIP `SymbolInformation.Kind` as follows: + +- `Constant` — Parameter, Constant, Loggroup +- `Variable` — Extern, Saved, Session, Local +- `Parameter` — MethodArg +- `Function` — Hook +- `Method` — Method +- `Class` — Template +- `TypeAlias` — Typedef +- `Namespace` — All composite objects (Device, Bank, Register, Field, Group, Port, Connect, Attribute, Event, Subdevice) +- `Struct` — Implement +- `Interface` — Interface + +#### Symbol Naming Scheme + +SCIP symbols follow the format: +` ' ' ' ' ' ' ' ' ` + +For DML, the scheme is `dml`, the manager is `simics`, version is `.` (currently we cannot extract simics version here), and the +package is the device name. Descriptors are built from the fully qualified path +through the device hierarchy: + +``` +dml simics sample_device . sample_device.regs.r1.offset. + ^ term (parameter) +dml simics sample_device . sample_device.regs.r1.read(). + ^ method +dml simics sample_device . bank# + ^ 'type' (template) +``` + +Descriptor suffixes follow the SCIP standard: +- `.` (term) — used for composite objects, parameters, and other named values +- `#` (type) — used only for templates +- `().` (method) — used for methods + +#### Local Symbols + +Method arguments and method-local variables use SCIP local symbols of the form +`local _`, where `` is the internal symbol identifier. Local +symbols are scoped to a single document and are not navigable across files. + +#### Occurrence Roles + +DML declarations and definitions are both emitted with the SCIP `Definition` +role, since SCIP does not distinguish between the two. References (including +template instantiation sites from `is` statements) are emitted with +`ReadAccess`. + +#### Relationships + +Composite objects that instantiate templates (via `is some_template`) emit +SCIP `Relationship` entries with `is_implementation = true` pointing to the +template symbol. diff --git a/src/actions/requests.rs b/src/actions/requests.rs index b4e2f65..f8bde14 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -942,6 +942,145 @@ impl RequestAction for GetKnownContextsRequest { } } +// ---- SCIP Export Request ---- + +#[derive(Debug, Clone)] +pub struct ExportScipRequest; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportScipParams { + /// Device paths to export SCIP for. If empty, exports all known devices. + pub devices: Option>, + /// The file path where the SCIP index should be written. + pub output_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportScipResult { + /// Whether the export succeeded. + pub success: bool, + /// Number of documents in the exported index. + pub document_count: usize, + /// Error message, if any. + pub error: Option, +} + +impl LSPRequest for ExportScipRequest { + type Params = ExportScipParams; + type Result = ExportScipResult; + + const METHOD: &'static str = "$/exportScip"; +} + +impl RequestAction for ExportScipRequest { + type Response = ExportScipResult; + + fn timeout() -> std::time::Duration { + crate::server::dispatch::DEFAULT_REQUEST_TIMEOUT * 30 + } + + fn fallback_response() -> Result { + Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some("Request timed out".to_string()), + }) + } + + fn get_identifier(params: &Self::Params) -> String { + Self::request_identifier(¶ms.output_path) + } + + fn handle( + ctx: InitActionContext, + params: Self::Params, + ) -> Result { + info!("Handling SCIP export request to {}", params.output_path); + + // Determine which device paths to export + let device_paths: Vec = + if let Some(devices) = params.devices { + devices.iter().filter_map( + |uri| parse_file_path!(&uri, "ExportScip") + .ok() + .and_then(CanonPath::from_path_buf)) + .collect() + } else { + vec![] + }; + + // Wait for device analyses to be ready + if !device_paths.is_empty() { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::Paths(device_paths.clone())).ok(); + } else { + ctx.wait_for_state( + AnalysisProgressKind::Device, + AnalysisWaitKind::Work, + AnalysisCoverageSpec::All).ok(); + } + + let analysis = ctx.analysis.lock().unwrap(); + + // Collect device analyses + let devices: Vec<&crate::analysis::DeviceAnalysis> = + if device_paths.is_empty() { + // Export all device analyses + analysis.device_analysis.values() + .map(|ts| &ts.stored) + .collect() + } else { + device_paths.iter().filter_map(|path| { + analysis.get_device_analysis(path).ok() + }).collect() + }; + + if devices.is_empty() { + return Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some("No device analyses found".to_string()), + }); + } + + info!("Exporting SCIP for {} device(s)", devices.len()); + + // Determine project root from workspaces + let project_root = ctx.workspace_roots + .lock() + .unwrap() + .first() + .and_then(|ws| parse_file_path!(&ws.uri, "ExportScip").ok()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + + let index = crate::scip::build_scip_index(&devices, &project_root); + let doc_count = index.documents.len(); + + let output = std::path::Path::new(¶ms.output_path); + match crate::scip::write_scip_to_file(index, output) { + Ok(()) => { + info!("SCIP export complete: {} documents written to {}", + doc_count, params.output_path); + Ok(ExportScipResult { + success: true, + document_count: doc_count, + error: None, + }) + }, + Err(e) => { + error!("SCIP export failed: {}", e); + Ok(ExportScipResult { + success: false, + document_count: 0, + error: Some(e), + }) + } + } + } +} + /// Server-to-client requests impl SentRequest for RegisterCapability { type Response = ::Result; diff --git a/src/cmd.rs b/src/cmd.rs index 0bf0c9d..f297323 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -322,6 +322,24 @@ pub fn set_contexts(paths: Vec) -> Notification, output_path: String) -> Request { + Request { + params: requests::ExportScipParams { + devices: if devices.is_empty() { + None + } else { + Some(devices.into_iter() + .map(|p| parse_uri(&p).unwrap()) + .collect()) + }, + output_path, + }, + action: PhantomData, + id: next_id(), + received: Instant::now(), + } +} + fn next_id() -> RequestId { static ID: AtomicU64 = AtomicU64::new(1); RequestId::Num(ID.fetch_add(1, Ordering::SeqCst)) diff --git a/src/dfa/client.rs b/src/dfa/client.rs index b404ee1..daaa363 100644 --- a/src/dfa/client.rs +++ b/src/dfa/client.rs @@ -414,4 +414,37 @@ impl ClientInterface { self.server.wait_timeout(Duration::from_millis(1000))?; Ok(()) } + + pub fn export_scip(&mut self, + device_paths: Vec, + output_path: String) + -> anyhow::Result { + debug!("Sending SCIP export request for {:?} -> {}", device_paths, output_path); + self.send( + cmd::export_scip(device_paths, output_path).to_string() + )?; + // Wait for the response + loop { + match self.receive_maybe() { + Ok(ServerMessage::Response(value)) => { + let result: crate::actions::requests::ExportScipResult + = serde_json::from_value(value) + .map_err(|e| RpcErrorKind::from(e.to_string()))?; + return Ok(result); + }, + Ok(ServerMessage::Error(e)) => { + return Err(anyhow::anyhow!( + "Server exited during SCIP export: {:?}", e)); + }, + Ok(_) => { + // Skip other messages (diagnostics, progress, etc.) + continue; + }, + Err(e) => { + trace!("Skipping message during SCIP export wait: {:?}", e); + continue; + } + } + } + } } diff --git a/src/dfa/main.rs b/src/dfa/main.rs index f373cd9..fbdbf48 100644 --- a/src/dfa/main.rs +++ b/src/dfa/main.rs @@ -40,6 +40,7 @@ struct Args { lint_cfg_path: Option, test: bool, quiet: bool, + scip_output: Option, } fn parse_args() -> Args { @@ -95,6 +96,11 @@ fn parse_args() -> Args { .action(ArgAction::Set) .value_parser(clap::value_parser!(PathBuf)) .required(false)) + .arg(Arg::new("scip-output").long("scip-output") + .help("Export SCIP index to the specified file after analysis") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .required(false)) .arg(arg!( ... "DML files to analyze") .value_parser(clap::value_parser!(PathBuf))) .arg_required_else_help(false) @@ -118,7 +124,9 @@ fn parse_args() -> Args { linting_enabled: args.get_one::("linting-enabled") .cloned(), lint_cfg_path: args.get_one::("lint-cfg-path") - .cloned() + .cloned(), + scip_output: args.get_one::("scip-output") + .cloned(), } } @@ -182,6 +190,33 @@ fn main_inner() -> Result<(), i32> { if arg.test && !dlsclient.no_errors() { exit_code = Err(1); } + + // Export SCIP if requested + if let Some(scip_path) = &arg.scip_output { + println!("Exporting SCIP index to {:?}", scip_path); + let scip_output_str = scip_path.to_string_lossy().to_string(); + let device_paths: Vec = arg.files.iter() + .filter_map(|f| f.canonicalize().ok()) + .map(|p| p.to_string_lossy().to_string()) + .collect(); + match dlsclient.export_scip(device_paths, scip_output_str) { + Ok(result) => { + if result.success { + println!("SCIP export complete: {} document(s) written", + result.document_count); + } else { + let err_msg = result.error.unwrap_or_else( + || "Unknown error".to_string()); + eprintln!("SCIP export failed: {}", err_msg); + exit_code = Err(1); + } + }, + Err(e) => { + eprintln!("SCIP export request failed: {}", e); + exit_code = Err(1); + } + } + } } // Disregard this result, we dont _really_ care about shutting down diff --git a/src/lib.rs b/src/lib.rs index 497835b..e6d7c8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub mod dfa; pub mod file_management; pub mod lint; pub mod lsp_data; +pub mod scip; pub mod server; pub mod span; pub mod utility; diff --git a/src/scip/mod.rs b/src/scip/mod.rs new file mode 100644 index 0000000..b63a969 --- /dev/null +++ b/src/scip/mod.rs @@ -0,0 +1,452 @@ +// © 2024 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 and MIT +//! SCIP (Source Code Intelligence Protocol) export support. +//! +//! This module converts DLS analysis data (DeviceAnalysis) into +//! the SCIP index format for use with code intelligence tools. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use protobuf::MessageField; +use protobuf::Enum; + +use scip::types::{ + Document, Index, Metadata, Occurrence, PositionEncoding, + Relationship, SymbolInformation, SymbolRole, ToolInfo, + symbol_information::Kind as ScipSymbolKind, +}; + +use crate::analysis::symbols::{DMLSymbolKind, SymbolSource}; +use crate::analysis::structure::objects::CompObjectKind; +use crate::analysis::templating::objects::{ + DMLHierarchyMember, DMLNamedMember, DMLObject, StructureContainer, +}; +use crate::analysis::DeviceAnalysis; +use crate::Span as ZeroSpan; + +use log::debug; + +/// Convert a ZeroSpan range into the SCIP occurrence range format. +/// +/// SCIP uses `[startLine, startChar, endLine, endChar]` (4 elements) +/// or `[startLine, startChar, endChar]` (3 elements, same-line). +/// All values are 0-based. +fn span_to_scip_range(span: &ZeroSpan) -> Vec { + let r = &span.range; + let start_line = r.row_start.0 as i32; + let start_char = r.col_start.0 as i32; + let end_line = r.row_end.0 as i32; + let end_char = r.col_end.0 as i32; + + if start_line == end_line { + vec![start_line, start_char, end_char] + } else { + vec![start_line, start_char, end_line, end_char] + } +} + +/// Map a DMLSymbolKind to a SCIP SymbolInformation Kind. +fn dml_kind_to_scip_kind(kind: &DMLSymbolKind) -> ScipSymbolKind { + match kind { + DMLSymbolKind::CompObject(comp_kind) => match comp_kind { + CompObjectKind::Interface => ScipSymbolKind::Interface, + CompObjectKind::Implement => ScipSymbolKind::Struct, + _ => ScipSymbolKind::Namespace, + }, + DMLSymbolKind::Parameter => ScipSymbolKind::Constant, + DMLSymbolKind::Constant => ScipSymbolKind::Constant, + DMLSymbolKind::Extern => ScipSymbolKind::Variable, + DMLSymbolKind::Hook => ScipSymbolKind::Function, + DMLSymbolKind::Local => ScipSymbolKind::Variable, + DMLSymbolKind::Loggroup => ScipSymbolKind::Constant, + DMLSymbolKind::Method => ScipSymbolKind::Method, + DMLSymbolKind::MethodArg => ScipSymbolKind::Parameter, + DMLSymbolKind::Saved => ScipSymbolKind::Variable, + DMLSymbolKind::Session => ScipSymbolKind::Variable, + DMLSymbolKind::Template => ScipSymbolKind::Class, + DMLSymbolKind::Typedef => ScipSymbolKind::TypeAlias, + } +} + +/// Sanitize a name for use in SCIP symbol strings. +/// +/// SCIP descriptors use backtick-escaping for names that contain +/// non-identifier characters, but to keep things simple we sanitize +/// to `[a-zA-Z0-9_]+`. +fn sanitize_name(name: &str) -> String { + name.chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }) + .collect() +} + +/// Build a `local` SCIP symbol string (document-scoped). +/// +/// Used for method arguments, method locals, and other symbols that +/// are only visible within a single file scope. +fn make_local_symbol(name: &str, id: u64) -> String { + format!("local {}_{}", sanitize_name(name), id) +} + +/// Build a global SCIP symbol string from a qualified path. +/// +/// Global symbols use the format: +/// `scheme ' ' manager ' ' package ' ' version ' ' descriptors...` +/// +/// We use: +/// - scheme: `dml` +/// - manager: `simics` +/// - package: device name +/// - version: `.` (single dot = no version) +/// - descriptors: built from the qualified path segments +/// +/// SCIP descriptor suffixes: +/// - `.` = namespace/term (banks, groups, etc.) +/// - `#` = type (templates, comp objects) +/// - `()` = method +fn make_global_symbol(device_name: &str, qualified_path: &str, + kind: &DMLSymbolKind) -> String { + let segments: Vec<&str> = qualified_path.split('.').collect(); + let mut descriptors = String::new(); + for (i, seg) in segments.iter().enumerate() { + let sanitized = sanitize_name(seg); + if i == segments.len() - 1 { + // Last segment gets suffix based on kind + match kind { + DMLSymbolKind::Method => { + descriptors.push_str(&sanitized); + descriptors.push_str("()."); + } + DMLSymbolKind::Template => { + // Templates are the type-like concept in DML + descriptors.push_str(&sanitized); + descriptors.push('#'); + } + _ => { + // Composite objects (device, bank, register, ...) + // are instances, not types — use term descriptor + descriptors.push_str(&sanitized); + descriptors.push('.'); + } + } + } else { + // Intermediate segments are namespace-like + descriptors.push_str(&sanitized); + descriptors.push('.'); + } + } + format!("dml simics {} . {}", sanitize_name(device_name), descriptors) +} + +/// Build the SCIP symbol string for a given SymbolSource. +/// +/// - DMLObject (comp or shallow): uses global symbol with qualified_name() +/// - Method: uses global symbol with parent's qualified_name + method name +/// - Template: uses global symbol at top level +/// - MethodArg / MethodLocal: uses local symbol +/// - Type: returns None (these are skipped) +fn scip_symbol_for_source( + source: &SymbolSource, + kind: &DMLSymbolKind, + id: u64, + device_name: &str, + container: &StructureContainer, +) -> Option<(String, String)> { + // Returns Some((scip_symbol, display_name)) + match source { + SymbolSource::DMLObject(dml_obj) => { + match dml_obj { + DMLObject::CompObject(key) => { + if let Some(comp) = container.get(*key) { + let qname = comp.qualified_name(container); + let display = comp.identity().to_string(); + let sym = make_global_symbol(device_name, + &qname, kind); + Some((sym, display)) + } else { + None + } + } + DMLObject::ShallowObject(shallow) => { + let qname = shallow.qualified_name(container); + let display = shallow.identity().to_string(); + let sym = make_global_symbol(device_name, + &qname, kind); + Some((sym, display)) + } + } + } + SymbolSource::Method(parent_key, methref) => { + let parent_qname = container.get(*parent_key) + .map(|p| p.qualified_name(container)) + .unwrap_or_default(); + let method_name = methref.identity(); + let qname = if parent_qname.is_empty() { + method_name.to_string() + } else { + format!("{}.{}", parent_qname, method_name) + }; + let sym = make_global_symbol( + device_name, &qname, &DMLSymbolKind::Method); + Some((sym, method_name.to_string())) + } + SymbolSource::Template(templ) => { + let sym = make_global_symbol( + device_name, &templ.name, &DMLSymbolKind::Template); + Some((sym, templ.name.clone())) + } + SymbolSource::MethodArg(_, name) => { + let sym = make_local_symbol(&name.val, id); + Some((sym, name.val.clone())) + } + SymbolSource::MethodLocal(_, name) => { + let sym = make_local_symbol(&name.val, id); + Some((sym, name.val.clone())) + } + SymbolSource::Type(_) => None, + } +} + +/// Build a human-readable documentation string for a DML symbol. +fn make_documentation(sym: &crate::analysis::symbols::Symbol, + display_name: &str) -> Vec { + let kind_str = format!("{:?}", sym.kind); + let typed_str = sym.typed.as_ref() + .map(|t| format!(" : {:?}", t)) + .unwrap_or_default(); + vec![format!("{} `{}`{}", kind_str, display_name, typed_str)] +} + +/// Holds per-file occurrence and symbol information data +/// that will be assembled into SCIP Documents. +#[derive(Default)] +struct FileData { + occurrences: Vec, + symbols: Vec, +} + +/// Convert a single DeviceAnalysis into SCIP Documents. +/// +/// Returns a tuple of (documents, external_symbols). Files under the +/// project root become Documents with relative paths; files outside +/// (e.g. Simics builtins) contribute only their SymbolInformation to +/// `external_symbols` for hover/navigation support. +fn device_analysis_to_documents( + device: &DeviceAnalysis, + project_root: &Path, +) -> (Vec, Vec) { + let mut file_data: HashMap = HashMap::new(); + let container = &device.objects; + let device_name = &device.name; + + // Iterate over all symbols in the device analysis + for symbol_ref in device.symbol_info.all_symbols() { + let sym = symbol_ref.symbol.lock().unwrap(); + + // Build the SCIP symbol and display name from the source + let (scip_symbol, display_name) = match scip_symbol_for_source( + &sym.source, &sym.kind, sym.id, device_name, container, + ) { + Some(pair) => pair, + None => continue, // Type symbols and unresolvable objects + }; + + debug!("SCIP symbol id={} kind={:?} scip={} defs={} decls={} refs={} impls={}", + sym.id, sym.kind, &scip_symbol, + sym.definitions.len(), sym.declarations.len(), + sym.references.len(), sym.implementations.len()); + + let kind = dml_kind_to_scip_kind(&sym.kind); + let documentation = make_documentation(&sym, &display_name); + + // Record the primary location as a definition occurrence + { + let loc = &sym.loc; + let file_path = loc.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(loc); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + + data.occurrences.push(occ); + + // Add SymbolInformation for this symbol (only once, at def site) + let mut sym_info = SymbolInformation::new(); + sym_info.symbol = scip_symbol.clone(); + sym_info.kind = kind.into(); + sym_info.display_name = display_name; + sym_info.documentation = documentation; + + // For comp objects, add Relationship entries for each + // instantiated template (`is` declarations). + if let SymbolSource::DMLObject( + DMLObject::CompObject(key)) = &sym.source { + if let Some(comp) = container.get(*key) { + for templ_name in comp.templates.keys() { + let templ_symbol = make_global_symbol( + device_name, templ_name, + &DMLSymbolKind::Template); + let mut rel = Relationship::new(); + rel.symbol = templ_symbol; + rel.is_implementation = true; + sym_info.relationships.push(rel); + } + } + } + + data.symbols.push(sym_info); + } + + // Record additional definitions + for def_span in &sym.definitions { + // Skip if same as primary loc + if *def_span == sym.loc { + continue; + } + let file_path = def_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(def_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::Definition.value(); + data.occurrences.push(occ); + } + + // Record declarations + for decl_span in &sym.declarations { + if *decl_span == sym.loc { + continue; + } + let file_path = decl_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(decl_span); + occ.symbol = scip_symbol.clone(); + // Declarations get the Definition role in SCIP + // (SCIP doesn't distinguish declaration vs definition) + occ.symbol_roles = SymbolRole::Definition.value(); + data.occurrences.push(occ); + } + + // Record references (read accesses) + for ref_span in &sym.references { + let file_path = ref_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(ref_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::ReadAccess.value(); + data.occurrences.push(occ); + } + + // Record implementation sites (`is template` occurrences) + // These are references to the template, not definitions. + // The actual implementation relationship is expressed via + // Relationship entries on the comp object's SymbolInformation. + for impl_span in &sym.implementations { + let file_path = impl_span.path(); + let data = file_data.entry(file_path).or_default(); + + let mut occ = Occurrence::new(); + occ.range = span_to_scip_range(impl_span); + occ.symbol = scip_symbol.clone(); + occ.symbol_roles = SymbolRole::ReadAccess.value(); + data.occurrences.push(occ); + } + } + + // Assemble Documents, separating in-project from external files. + let mut documents = Vec::new(); + let mut external_symbols = Vec::new(); + + for (path, data) in file_data { + match path.strip_prefix(project_root) { + Ok(rel) => { + let mut doc = Document::new(); + doc.relative_path = rel.to_string_lossy().to_string(); + doc.language = "dml".to_string(); + doc.position_encoding = + PositionEncoding::UTF8CodeUnitOffsetFromLineStart.into(); + doc.occurrences = data.occurrences; + doc.symbols = data.symbols; + documents.push(doc); + } + Err(_) => { + // External file: keep symbol info for hover/navigation + // but don't emit a document or occurrences + external_symbols.extend(data.symbols); + } + } + } + + (documents, external_symbols) +} + +/// Build a complete SCIP Index from one or more DeviceAnalyses. +/// +/// # Arguments +/// * `devices` - The device analyses to export +/// * `project_root` - The workspace root path, used to compute relative paths +pub fn build_scip_index( + devices: &[&DeviceAnalysis], + project_root: &Path, +) -> Index { + debug!("Building SCIP index for {} device(s) rooted at {:?}", + devices.len(), project_root); + + let mut tool_info = ToolInfo::new(); + tool_info.name = "dls".to_string(); + tool_info.version = crate::version(); + + let mut metadata = Metadata::new(); + metadata.tool_info = MessageField::some(tool_info); + let root_str = project_root.to_string_lossy(); + metadata.project_root = if root_str.ends_with('/') { + format!("file://{}", root_str) + } else { + format!("file://{}/", root_str) + }; + metadata.text_document_encoding = scip::types::TextEncoding::UTF8.into(); + + // Collect documents from all devices, merging by relative_path + let mut merged_docs: HashMap = HashMap::new(); + let mut all_external_symbols: Vec = Vec::new(); + + for device in devices { + let (docs, ext_syms) = device_analysis_to_documents(device, project_root); + for doc in docs { + let entry = merged_docs.entry(doc.relative_path.clone()) + .or_insert_with(|| { + let mut d = Document::new(); + d.relative_path = doc.relative_path.clone(); + d.language = doc.language.clone(); + d.position_encoding = doc.position_encoding; + d + }); + entry.occurrences.extend(doc.occurrences); + entry.symbols.extend(doc.symbols); + } + all_external_symbols.extend(ext_syms); + } + + let mut index = Index::new(); + index.metadata = MessageField::some(metadata); + index.documents = merged_docs.into_values().collect(); + index.external_symbols = all_external_symbols; + + debug!("SCIP index built with {} document(s)", index.documents.len()); + index +} + +/// Write a SCIP index to a file. +pub fn write_scip_to_file(index: Index, output_path: &Path) + -> Result<(), String> { + debug!("Writing SCIP index to {:?}", output_path); + scip::write_message_to_file(output_path, index) + .map_err(|e| format!("Failed to write SCIP index: {}", e)) +} diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 514da63..afc2d1a 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -113,6 +113,7 @@ define_dispatch_request_enum!( ExecuteCommand, CodeLensRequest, GetKnownContextsRequest, + ExportScipRequest, ); /// Provides ability to dispatch requests to a worker thread that will diff --git a/src/server/mod.rs b/src/server/mod.rs index 582a9ef..4b671e5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -594,7 +594,8 @@ impl LsService { requests::References, requests::Completion, requests::CodeLensRequest, - requests::GetKnownContextsRequest; + requests::GetKnownContextsRequest, + requests::ExportScipRequest; ); Ok(()) }