Skip to content

Commit 5abe282

Browse files
committed
refactor(app): extract command generation logic
1 parent 0b2ea71 commit 5abe282

File tree

4 files changed

+403
-335
lines changed

4 files changed

+403
-335
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use serde_json::{Value, json};
2+
use tower_lsp::lsp_types::{Location, Range, Url};
3+
4+
use crate::app::lsp_server::supported_commands::SupportedCommands;
5+
use crate::infra::{parse_compose_file, parse_dockerfile};
6+
7+
pub struct CommandInfo {
8+
pub title: String,
9+
pub command: String,
10+
pub arguments: Option<Vec<Value>>,
11+
pub range: Range,
12+
}
13+
14+
impl From<SupportedCommands> for CommandInfo {
15+
fn from(value: SupportedCommands) -> Self {
16+
match &value {
17+
SupportedCommands::ExecuteBaseImageScan { location, image } => CommandInfo {
18+
title: "Scan base image".to_owned(),
19+
command: value.as_string_command(),
20+
arguments: Some(vec![json!(location), json!(image)]),
21+
range: location.range,
22+
},
23+
24+
SupportedCommands::ExecuteBuildAndScan { location } => CommandInfo {
25+
title: "Build and scan".to_owned(),
26+
command: value.as_string_command(),
27+
arguments: Some(vec![json!(location)]),
28+
range: location.range,
29+
},
30+
}
31+
}
32+
}
33+
34+
pub fn generate_commands_for_uri(uri: &Url, content: &str) -> Vec<CommandInfo> {
35+
let file_uri = uri.as_str();
36+
37+
if file_uri.contains("docker-compose.yml")
38+
|| file_uri.contains("compose.yml")
39+
|| file_uri.contains("docker-compose.yaml")
40+
|| file_uri.contains("compose.yaml")
41+
{
42+
generate_compose_commands(uri, content)
43+
} else {
44+
generate_dockerfile_commands(uri, content)
45+
}
46+
}
47+
48+
fn generate_compose_commands(url: &Url, content: &str) -> Vec<CommandInfo> {
49+
let mut commands = vec![];
50+
if let Ok(instructions) = parse_compose_file(content) {
51+
for instruction in instructions {
52+
commands.push(
53+
SupportedCommands::ExecuteBaseImageScan {
54+
location: Location::new(url.clone(), instruction.range),
55+
image: instruction.image_name,
56+
}
57+
.into(),
58+
);
59+
}
60+
}
61+
commands
62+
}
63+
64+
fn generate_dockerfile_commands(uri: &Url, content: &str) -> Vec<CommandInfo> {
65+
let mut commands = vec![];
66+
let instructions = parse_dockerfile(content);
67+
if let Some(last_from_instruction) = instructions
68+
.iter()
69+
.filter(|instruction| instruction.keyword == "FROM")
70+
.next_back()
71+
{
72+
let range = last_from_instruction.range;
73+
commands.push(
74+
SupportedCommands::ExecuteBuildAndScan {
75+
location: Location::new(uri.clone(), range),
76+
}
77+
.into(),
78+
);
79+
if let Some(image) = last_from_instruction.arguments.first() {
80+
commands.push(
81+
SupportedCommands::ExecuteBaseImageScan {
82+
location: Location::new(uri.clone(), range),
83+
image: image.to_owned(),
84+
}
85+
.into(),
86+
);
87+
}
88+
}
89+
commands
90+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
use std::path::PathBuf;
2+
use std::str::FromStr;
3+
4+
use serde_json::Value;
5+
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
6+
use tower_lsp::lsp_types::{
7+
CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse,
8+
CodeLens, CodeLensOptions, CodeLensParams, Command, DidChangeConfigurationParams,
9+
DidChangeTextDocumentParams, DidOpenTextDocumentParams, ExecuteCommandOptions,
10+
ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
11+
Range, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
12+
};
13+
use tracing::{debug, info};
14+
15+
use super::super::commands::CommandExecutor;
16+
use super::super::component_factory::{ComponentFactory, Config};
17+
use super::super::queries::QueryExecutor;
18+
use super::command_generator;
19+
use super::{InMemoryDocumentDatabase, LSPClient, WithContext};
20+
21+
use super::supported_commands::SupportedCommands;
22+
23+
pub struct LSPServerInner<C> {
24+
command_executor: CommandExecutor<C>,
25+
query_executor: QueryExecutor,
26+
component_factory: ComponentFactory,
27+
}
28+
29+
impl<C> LSPServerInner<C> {
30+
pub fn new(client: C) -> LSPServerInner<C> {
31+
let document_database = InMemoryDocumentDatabase::default();
32+
33+
LSPServerInner {
34+
command_executor: CommandExecutor::new(client, document_database.clone()),
35+
query_executor: QueryExecutor::new(document_database.clone()),
36+
component_factory: Default::default(), // to be initialized in the initialize method of the LSP
37+
}
38+
}
39+
}
40+
41+
impl<C> LSPServerInner<C>
42+
where
43+
C: LSPClient + Send + Sync + 'static,
44+
{
45+
async fn initialize_component_factory_with(&mut self, config: &Value) -> Result<()> {
46+
let Ok(config) = serde_json::from_value::<Config>(config.clone()) else {
47+
return Err(Error::internal_error()
48+
.with_message(format!("unable to transform json into config: {config}")));
49+
};
50+
51+
debug!("updating with configuration: {config:?}");
52+
53+
self.component_factory.initialize_with(config);
54+
55+
debug!("updated configuration");
56+
Ok(())
57+
}
58+
}
59+
60+
impl<C> LSPServerInner<C>
61+
where
62+
C: LSPClient + Send + Sync + 'static,
63+
{
64+
pub async fn initialize(
65+
&mut self,
66+
initialize_params: InitializeParams,
67+
) -> Result<InitializeResult> {
68+
let Some(config) = initialize_params.initialization_options else {
69+
return Err(Error {
70+
code: ErrorCode::InvalidParams,
71+
message: "expected parameters to configure the LSP, received nothing".into(),
72+
data: None,
73+
});
74+
};
75+
76+
self.initialize_component_factory_with(&config).await?;
77+
78+
Ok(InitializeResult {
79+
capabilities: ServerCapabilities {
80+
text_document_sync: Some(TextDocumentSyncCapability::Kind(
81+
TextDocumentSyncKind::FULL,
82+
)),
83+
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
84+
code_lens_provider: Some(CodeLensOptions {
85+
resolve_provider: Some(false),
86+
}),
87+
execute_command_provider: Some(ExecuteCommandOptions {
88+
commands: SupportedCommands::all_supported_commands_as_string(),
89+
..Default::default()
90+
}),
91+
..Default::default()
92+
},
93+
..Default::default()
94+
})
95+
}
96+
97+
pub async fn initialized(&self, _: InitializedParams) {
98+
info!("Initialized");
99+
self.command_executor
100+
.show_message(MessageType::INFO, "Sysdig LSP initialized")
101+
.await;
102+
}
103+
104+
pub async fn did_change_configuration(&mut self, params: DidChangeConfigurationParams) {
105+
let _ = self
106+
.initialize_component_factory_with(&params.settings)
107+
.await;
108+
}
109+
110+
pub async fn did_open(&self, params: DidOpenTextDocumentParams) {
111+
self.command_executor
112+
.update_document_with_text(
113+
params.text_document.uri.as_str(),
114+
params.text_document.text.as_str(),
115+
)
116+
.await;
117+
}
118+
119+
pub async fn did_change(&self, params: DidChangeTextDocumentParams) {
120+
if let Some(change) = params.content_changes.into_iter().next_back() {
121+
self.command_executor
122+
.update_document_with_text(params.text_document.uri.as_str(), &change.text)
123+
.await;
124+
}
125+
}
126+
127+
pub async fn code_action(
128+
&self,
129+
params: CodeActionParams,
130+
) -> Result<Option<CodeActionResponse>> {
131+
let Some(content) = self
132+
.query_executor
133+
.get_document_text(params.text_document.uri.as_str())
134+
.await
135+
else {
136+
return Err(Error::internal_error().with_message(format!(
137+
"unable to extract document content for document: {}",
138+
&params.text_document.uri
139+
)));
140+
};
141+
142+
let commands =
143+
command_generator::generate_commands_for_uri(&params.text_document.uri, &content);
144+
let code_actions: Vec<CodeActionOrCommand> = commands
145+
.into_iter()
146+
.filter(|cmd| cmd.range.start.line == params.range.start.line)
147+
.map(|cmd| {
148+
CodeActionOrCommand::Command(Command {
149+
title: cmd.title,
150+
command: cmd.command,
151+
arguments: cmd.arguments,
152+
})
153+
})
154+
.collect();
155+
156+
Ok(Some(code_actions))
157+
}
158+
159+
pub async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
160+
let Some(content) = self
161+
.query_executor
162+
.get_document_text(params.text_document.uri.as_str())
163+
.await
164+
else {
165+
return Err(Error::internal_error().with_message(format!(
166+
"unable to extract document content for document: {}",
167+
&params.text_document.uri
168+
)));
169+
};
170+
171+
let commands =
172+
command_generator::generate_commands_for_uri(&params.text_document.uri, &content);
173+
let code_lenses = commands
174+
.into_iter()
175+
.map(|cmd| CodeLens {
176+
range: cmd.range,
177+
command: Some(Command {
178+
title: cmd.title,
179+
command: cmd.command,
180+
arguments: cmd.arguments,
181+
}),
182+
data: None,
183+
})
184+
.collect();
185+
186+
Ok(Some(code_lenses))
187+
}
188+
189+
pub async fn execute_command(&mut self, params: ExecuteCommandParams) -> Result<Option<Value>> {
190+
let command: SupportedCommands = params.try_into()?;
191+
192+
let result = match command.clone() {
193+
SupportedCommands::ExecuteBaseImageScan { location, image } => {
194+
execute_command_scan_base_image(
195+
self,
196+
location.uri.to_string(),
197+
location.range,
198+
image,
199+
)
200+
.await
201+
.map(|_| None)
202+
}
203+
204+
SupportedCommands::ExecuteBuildAndScan { location } => {
205+
execute_command_build_and_scan(self, location.uri.to_string(), location.range)
206+
.await
207+
.map(|_| None)
208+
}
209+
};
210+
211+
match result {
212+
Ok(_) => result,
213+
Err(mut e) => {
214+
self.command_executor
215+
.show_message(MessageType::ERROR, e.to_string().as_str())
216+
.await;
217+
e.message = format!("error calling command: '{command}': {e}").into();
218+
Err(e)
219+
}
220+
}
221+
}
222+
223+
pub async fn shutdown(&self) -> Result<()> {
224+
Ok(())
225+
}
226+
}
227+
228+
async fn execute_command_scan_base_image<C: LSPClient>(
229+
server: &mut LSPServerInner<C>,
230+
file: String,
231+
range: Range,
232+
image: String,
233+
) -> Result<()> {
234+
let image_scanner = {
235+
server.component_factory.image_scanner().map_err(|e| {
236+
Error::internal_error().with_message(format!("unable to create image scanner: {e}"))
237+
})?
238+
};
239+
240+
server
241+
.command_executor
242+
.scan_image(&file, range, &image, &image_scanner)
243+
.await?;
244+
245+
Ok(())
246+
}
247+
248+
async fn execute_command_build_and_scan<C: LSPClient>(
249+
server: &mut LSPServerInner<C>,
250+
file: String,
251+
range: Range,
252+
) -> Result<()> {
253+
let (image_scanner, image_builder) = {
254+
let image_scanner = server.component_factory.image_scanner().map_err(|e| {
255+
Error::internal_error().with_message(format!("unable to create image scanner: {e}"))
256+
})?;
257+
let image_builder = server.component_factory.image_builder().map_err(|e| {
258+
Error::internal_error().with_message(format!("unable to create image builder: {e}"))
259+
})?;
260+
261+
(image_scanner, image_builder)
262+
};
263+
264+
server
265+
.command_executor
266+
.build_and_scan_from_file(
267+
&PathBuf::from_str(&file).unwrap(),
268+
range.start.line,
269+
&image_builder,
270+
&image_scanner,
271+
)
272+
.await?;
273+
274+
Ok(())
275+
}

0 commit comments

Comments
 (0)