Skip to content
3 changes: 2 additions & 1 deletion crates/graphcal-lsp/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ pub struct AnalysisResult {
pub eval_values: HashMap<ScopedName, String>,
/// The document's view model (structure, and values when it evaluated),
/// served to the `graphcal/viewModel` request. `None` when the project
/// did not check.
/// did not check, or when the client shows no view models (the analysis
/// then skips the projection).
pub view: Option<graphcal_project::view_model::ViewModel>,
/// Structured function signatures, keyed by function name.
/// Points to a lazily-initialized static map (builtins never change).
Expand Down
58 changes: 50 additions & 8 deletions crates/graphcal-lsp/src/analysis_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ impl AnalysisDegradation {
}
}

/// Whether an analysis also projects the document's view model.
///
/// The view is experimental: only a client that shows view models
/// (`experimental.graphcal.viewModelChanges`) pays for the projection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewModelDemand {
/// No view is shown: the analysis carries no view model.
Skip,
/// The client shows view models: project one after checking.
Project,
}

/// Transient output of one analysis pass.
pub struct AnalysisRun {
pub analysis: AnalysisResult,
Expand Down Expand Up @@ -258,6 +270,7 @@ pub(crate) fn run_analysis_run(
open_buffers,
plugin_host,
&input_snapshot,
ViewModelDemand::Project,
&graphcal_compiler::cancellation::CancellationToken::unbounded(),
)
.expect("an unbounded analysis cannot be cancelled")
Expand Down Expand Up @@ -287,6 +300,7 @@ pub fn run_analysis_with_cancellation(
open_buffers: &[OpenBuffer],
plugin_host: &graphcal_plugin_host::PluginHost,
input_snapshot: &AnalysisInputSnapshot,
view: ViewModelDemand,
cancellation: &CancellationToken,
) -> std::result::Result<AnalysisRun, Cancelled> {
cancellation.checkpoint()?;
Expand Down Expand Up @@ -396,14 +410,17 @@ pub fn run_analysis_with_cancellation(
// user opens such a file for editing.
let todos =
crate::diagnostics::unfinished_node_diagnostics(&root_ast.declarations, text);
let (mut view, degradations) = match project_view_for_lsp(&project, &checked) {
Ok(view) => (Some(view), Vec::new()),
Err(error) => (
None,
vec![AnalysisDegradation::ViewUnavailable {
reason: error.to_string(),
}],
),
let (mut view, degradations) = match view {
ViewModelDemand::Skip => (None, Vec::new()),
ViewModelDemand::Project => match project_view_for_lsp(&project, &checked) {
Ok(view) => (Some(view), Vec::new()),
Err(error) => (
None,
vec![AnalysisDegradation::ViewUnavailable {
reason: error.to_string(),
}],
),
},
};
cancellation.checkpoint()?;
let (mut diagnostics, eval_values) = if checked.is_library() {
Expand Down Expand Up @@ -625,4 +642,29 @@ mod view_tests {
assert_eq!(names, ["thrust", "doubled"]);
assert!(view.nodes[1].value.is_some(), "doubled should be evaluated");
}

#[test]
fn analysis_skips_the_view_model_unless_asked() {
let uri = Url::parse("untitled:view.gcl").unwrap();
let input_snapshot = AnalysisInputSnapshot::new(
crate::file_identity::document_identity(&uri),
crate::workspace_revision::RevisionClock::default()
.next()
.unwrap(),
HashMap::new(),
);
let run = run_analysis_with_cancellation(
&uri,
"node thrust: Dimensionless = 1.0;\n",
&[],
test_plugin_host(),
&input_snapshot,
ViewModelDemand::Skip,
&CancellationToken::unbounded(),
)
.unwrap();
assert_eq!(run.degradations, Vec::<AnalysisDegradation>::new());
assert!(run.analysis.view.is_none());
assert!(run.analysis.has_no_diagnostics());
}
}
47 changes: 24 additions & 23 deletions crates/graphcal-lsp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ use tower_lsp::lsp_types::{
use tower_lsp::{Client, ClientSocket, LanguageServer, LspService, Server};

use crate::analysis::AnalysisResult;
use crate::analysis_pipeline::{AnalysisRun, OpenBuffer, run_analysis_with_cancellation};
use crate::analysis_pipeline::{
AnalysisRun, OpenBuffer, ViewModelDemand, run_analysis_with_cancellation,
};
use crate::analysis_schedule_state::AnalysisScheduleState;
use crate::client_capabilities::{ClientFeatureSupport, DocumentSymbolShape, WorkspaceEditShape};
use crate::convert::position_to_byte_offset;
Expand Down Expand Up @@ -730,6 +732,11 @@ async fn analyze_store_publish_once(
}

let uri_for_analysis = uri.clone();
let view = if client_features.view_model_changes.is_supported() {
ViewModelDemand::Project
} else {
ViewModelDemand::Skip
};
let source = registration.source;
let worker_plugin_host = Arc::clone(plugin_host);
let mut task = tokio::task::spawn_blocking(move || {
Expand All @@ -743,6 +750,7 @@ async fn analyze_store_publish_once(
&open_buffers,
&worker_plugin_host,
&input_snapshot,
view,
&cancellation,
)
});
Expand Down Expand Up @@ -1494,8 +1502,9 @@ pub(crate) fn service() -> (LspService<Backend>, ClientSocket) {
}

/// `graphcal/viewModel`: the view model of a document, as of its latest
/// analysis. `null` until the document has been analyzed, or when its
/// project did not check.
/// analysis. `null` until the document has been analyzed, when its project
/// did not check, or when the client did not declare
/// `experimental.graphcal.viewModelChanges` (analysis then skips the view).
enum ViewModelRequest {}

impl ViewModelRequest {
Expand Down Expand Up @@ -3510,20 +3519,21 @@ node momentum: Force * Time = @mass * @velocity;
root_revision,
HashMap::from([(dependency.clone(), old_dependency_revision)]),
);
let analyze = |overlay: &[OpenBuffer], input: &AnalysisInputSnapshot| {
let unbounded = CancellationToken::unbounded();
let host = test_plugin_host();
let view = ViewModelDemand::Skip;
run_analysis_with_cancellation(
&main_uri, &main_text, overlay, host, input, view, &unbounded,
)
.unwrap()
.analysis
};
let overlay = vec![OpenBuffer {
path: lib_path,
text: Arc::new("pub const node y: Dimensionless = 2.0;\n".to_string()),
}];
let analysis = run_analysis_with_cancellation(
&main_uri,
&main_text,
&overlay,
test_plugin_host(),
&input,
&CancellationToken::unbounded(),
)
.unwrap()
.analysis;
let analysis = analyze(&overlay, &input);
assert!(analysis.has_no_diagnostics());
assert!(
analysis
Expand All @@ -3549,16 +3559,7 @@ node momentum: Force * Time = @mass * @velocity;
path: dir.path().join("src/fresh/lib.gcl"),
text: Arc::new("pub const node q: Dimensionless = 4.0;\n".to_string()),
}];
let refreshed = run_analysis_with_cancellation(
&main_uri,
&main_text,
&refreshed_overlay,
test_plugin_host(),
&refreshed_input,
&CancellationToken::unbounded(),
)
.unwrap()
.analysis;
let refreshed = analyze(&refreshed_overlay, &refreshed_input);
assert!(!refreshed.has_no_diagnostics());
let use_offset = main_text.rfind('y').unwrap();
assert!(goto_definition::goto_definition(&analysis, &main_uri, use_offset).is_some());
Expand Down
4 changes: 2 additions & 2 deletions crates/graphcal-project/src/view_model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod tests;
mod value;

pub use model::{
ViewEntry, ViewIndex, ViewModel, ViewModule, ViewNode, ViewNodeKind, ViewPosition, ViewRange,
ViewReduce, ViewSource, ViewValue,
ViewEntry, ViewIndex, ViewInstance, ViewModel, ViewModule, ViewNode, ViewNodeKind,
ViewPosition, ViewRange, ViewReduce, ViewSource, ViewValue,
};
pub use project::{ViewProjection, ViewProjectionError, project_view};
21 changes: 16 additions & 5 deletions crates/graphcal-project/src/view_model/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,10 @@ pub struct ViewNode {
pub expr: Option<String>,
/// The aggregation that removed an axis of a dependency. Not derived yet.
pub reduce: Option<ViewReduce>,
/// For an external read through an `include`: the instance's name
/// (`eps` in `include power_project.eps() …`, or the alias of
/// `include … as e`). `None` for the module's own declarations and for
/// imports, which have no instance.
pub instance: Option<String>,
/// For an external read through an `include`: the instance it is read
/// through. `None` for the module's own declarations and for imports,
/// which have no instance.
pub instance: Option<ViewInstance>,
/// The declaration's `///` documentation: one line per `///` line,
/// joined with newlines. The view shows its first line beside the name.
pub doc: Option<String>,
Expand All @@ -94,6 +93,18 @@ pub struct ViewNode {
pub value: Option<ViewValue>,
}

/// An include instance a node is read through.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ViewInstance {
/// The instance's name: `eps` in `include power_project.eps() …`, or
/// the alias of `include … as e`.
pub name: String,
/// Whether the include binds any of the template's params or Static
/// parameters. A bound instance's values differ from those of its
/// module evaluated alone, which is all a view of that module shows.
pub bound: bool,
}

/// An aggregation over one axis.
#[derive(Debug, Clone, Serialize)]
pub struct ViewReduce {
Expand Down
Loading
Loading