Skip to content

Commit 73f5b69

Browse files
authored
feat: add code lens support (#3)
1 parent 0fba0e5 commit 73f5b69

File tree

9 files changed

+274
-114
lines changed

9 files changed

+274
-114
lines changed

Cargo.lock

Lines changed: 104 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sysdig-lsp"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2024"
55

66

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm
1818
| Feature | **[VSCode Extension](https://github.com/sysdiglabs/vscode-extension)** | **Sysdig LSP** |
1919
|---------------------------------|------------------------------------------------------------------------|----------------------------------------------------------|
2020
| Scan base image in Dockerfile | Supported | [Supported](./docs/features/scan_base_image.md) (0.1.0+) |
21-
| Code lens support | Supported | In roadmap |
21+
| Code lens support | Supported | [Supported](./docs/features/code_lens.md) (0.2.0+) |
2222
| Build and Scan Dockerfile | Supported | In roadmap |
2323
| Layered image analysis | Supported | In roadmap |
2424
| Docker-compose image analysis | Supported | In roadmap |

docs/features/code_lens.gif

1.03 MB
Loading

docs/features/code_lens.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Code Lens Support
2+
3+
A CodeLens is a feature that displays additional information about the code,
4+
such as suggestions, references, or action buttons, directly in the editor without modifying the code itself.
5+
6+
For example, in VS Code, you might see something like "Used 3 times" above a function,
7+
with an option to click and view references.
8+
9+
In our case, for the **Sysdig LSP**, **CodeLens** allows executing actions that can also be
10+
performed with **Code Actions** (like [scanning the base image](./scan_base_image.md)), but in a more graphical way by simply clicking.
11+
This enhances usability by providing direct interaction within the editor.
12+
13+
![Sysdig LSP code lens for base image scanning](./code_lens.gif)

src/app/commands.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ where
3535
C: LSPClient,
3636
{
3737
pub async fn update_document_with_text(&self, uri: &str, text: &str) {
38-
self.document_database.remove_document(uri).await;
3938
self.document_database.write_document_text(uri, text).await;
39+
self.document_database.remove_diagnostics(uri).await;
4040
let _ = self.publish_all_diagnostics().await;
4141
}
4242

src/app/lsp_server.rs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use tower_lsp::LanguageServer;
66
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
77
use tower_lsp::lsp_types::{
88
CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse,
9-
Command, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
10-
ExecuteCommandOptions, ExecuteCommandParams, InitializeParams, InitializeResult,
11-
InitializedParams, MessageType, ServerCapabilities, TextDocumentSyncCapability,
12-
TextDocumentSyncKind,
9+
CodeLens, CodeLensOptions, CodeLensParams, Command, DidChangeConfigurationParams,
10+
DidChangeTextDocumentParams, DidOpenTextDocumentParams, ExecuteCommandOptions,
11+
ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
12+
Position, Range, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
1313
};
1414
use tracing::{debug, info};
1515

@@ -106,6 +106,9 @@ where
106106
TextDocumentSyncKind::FULL,
107107
)),
108108
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
109+
code_lens_provider: Some(CodeLensOptions {
110+
resolve_provider: Some(false),
111+
}),
109112
execute_command_provider: Some(ExecuteCommandOptions {
110113
commands: vec![SupportedCommands::ExecuteScan.to_string()],
111114
..Default::default()
@@ -180,7 +183,7 @@ where
180183

181184
if last_line_starting_with_from_statement == line_selected_as_usize {
182185
let action = Command {
183-
title: "Scan Image".to_string(),
186+
title: "Scan base image".to_string(),
184187
command: SupportedCommands::ExecuteScan.to_string(),
185188
arguments: Some(vec![
186189
json!(params.text_document.uri),
@@ -194,6 +197,58 @@ where
194197
return Ok(None);
195198
}
196199

200+
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
201+
info!("{}", format!("received code lens params: {params:?}"));
202+
203+
let Some(content) = self
204+
.query_executor
205+
.get_document_text(params.text_document.uri.as_str())
206+
.await
207+
else {
208+
return Err(lsp_error(
209+
ErrorCode::InternalError,
210+
format!(
211+
"unable to extract document content for document: {}",
212+
&params.text_document.uri
213+
),
214+
));
215+
};
216+
217+
let Some(last_line_starting_with_from_statement) = content
218+
.lines()
219+
.enumerate()
220+
.filter(|(_, line)| line.trim_start().starts_with("FROM "))
221+
.map(|(line_num, _)| line_num)
222+
.last()
223+
else {
224+
return Ok(None);
225+
};
226+
227+
let scan_base_image_lens = CodeLens {
228+
range: Range {
229+
start: Position {
230+
line: last_line_starting_with_from_statement as u32,
231+
character: 0,
232+
},
233+
end: Position {
234+
line: last_line_starting_with_from_statement as u32,
235+
character: 0,
236+
},
237+
},
238+
command: Some(Command {
239+
title: "Scan base image".to_string(),
240+
command: SupportedCommands::ExecuteScan.to_string(),
241+
arguments: Some(vec![
242+
json!(params.text_document.uri),
243+
json!(last_line_starting_with_from_statement),
244+
]),
245+
}),
246+
data: None,
247+
};
248+
249+
Ok(Some(vec![scan_base_image_lens]))
250+
}
251+
197252
async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
198253
let command: SupportedCommands = params.command.as_str().try_into().map_err(|e| {
199254
lsp_error(

tests/general.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use serde_json::json;
2-
use tower_lsp::lsp_types::{CodeActionOrCommand, Command, MessageType};
2+
use tower_lsp::lsp_types::{CodeActionOrCommand, CodeLens, Command, MessageType, Position, Range};
33

44
mod test;
55

@@ -34,7 +34,7 @@ async fn when_the_client_asks_for_the_existing_code_actions_it_receives_the_avai
3434
assert_eq!(
3535
response.unwrap(),
3636
vec![CodeActionOrCommand::Command(Command {
37-
title: "Scan Image".to_string(),
37+
title: "Scan base image".to_string(),
3838
command: "sysdig-lsp.execute-scan".to_string(),
3939
arguments: Some(vec![json!("file://dockerfile/"), json!(0)])
4040
})]
@@ -62,9 +62,82 @@ async fn when_the_client_asks_for_the_existing_code_actions_but_the_dockerfile_c
6262
assert_eq!(
6363
response_for_second_line.unwrap(),
6464
vec![CodeActionOrCommand::Command(Command {
65-
title: "Scan Image".to_string(),
65+
title: "Scan base image".to_string(),
6666
command: "sysdig-lsp.execute-scan".to_string(),
6767
arguments: Some(vec![json!("file://dockerfile/"), json!(1)])
6868
})]
6969
);
7070
}
71+
72+
#[tokio::test]
73+
async fn when_the_client_asks_for_the_existing_code_lens_it_receives_the_available_code_lens() {
74+
let mut client = test::TestClient::new_initialized().await;
75+
76+
// Open a Dockerfile containing a single "FROM" statement.
77+
client
78+
.open_file_with_contents("Dockerfile", "FROM alpine")
79+
.await;
80+
81+
// Request code lens on the line with the FROM statement (line 0).
82+
let response = client
83+
.request_available_code_lens_in_file("Dockerfile")
84+
.await;
85+
86+
// Expect a CodeLens with the appropriate command.
87+
assert_eq!(
88+
response.unwrap(),
89+
vec![CodeLens {
90+
range: Range {
91+
start: Position {
92+
line: 0,
93+
character: 0
94+
},
95+
end: Position {
96+
line: 0,
97+
character: 0
98+
}
99+
},
100+
command: Some(Command {
101+
title: "Scan base image".to_string(),
102+
command: "sysdig-lsp.execute-scan".to_string(),
103+
arguments: Some(vec![json!("file://dockerfile/"), json!(0)])
104+
}),
105+
data: None
106+
}]
107+
);
108+
}
109+
110+
#[tokio::test]
111+
async fn when_the_client_asks_for_the_existing_code_lens_but_the_dockerfile_contains_multiple_froms_it_only_returns_the_latest()
112+
{
113+
let mut client = test::TestClient::new_initialized().await;
114+
client
115+
.open_file_with_contents("Dockerfile", "FROM alpine\nFROM ubuntu")
116+
.await;
117+
118+
let response = client
119+
.request_available_code_lens_in_file("Dockerfile")
120+
.await;
121+
122+
assert_eq!(
123+
response.unwrap(),
124+
vec![CodeLens {
125+
range: Range {
126+
start: Position {
127+
line: 1,
128+
character: 0
129+
},
130+
end: Position {
131+
line: 1,
132+
character: 0
133+
}
134+
},
135+
command: Some(Command {
136+
title: "Scan base image".to_string(),
137+
command: "sysdig-lsp.execute-scan".to_string(),
138+
arguments: Some(vec![json!("file://dockerfile/"), json!(1)])
139+
}),
140+
data: None
141+
}]
142+
);
143+
}

tests/test.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use sysdig_lsp::app::{LSPClient, LSPServer};
88
use tokio::sync::Mutex;
99
use tower_lsp::LanguageServer;
1010
use tower_lsp::lsp_types::{
11-
CodeActionOrCommand, CodeActionParams, Diagnostic, DidOpenTextDocumentParams, InitializeParams,
12-
InitializeResult, InitializedParams, MessageType, Position, Range, TextDocumentIdentifier,
13-
TextDocumentItem, Url,
11+
CodeActionOrCommand, CodeActionParams, CodeLens, CodeLensParams, Diagnostic,
12+
DidOpenTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
13+
Position, Range, TextDocumentIdentifier, TextDocumentItem, Url,
1414
};
1515

1616
pub struct TestClient {
@@ -86,11 +86,24 @@ impl TestClient {
8686
.await
8787
.unwrap_or_else(|_| {
8888
panic!(
89-
"unable to send code action for filename {} in line number {}",
90-
filename, line_number
89+
"unable to send code action for filename {filename} in line number {line_number}",
9190
)
9291
})
9392
}
93+
94+
pub async fn request_available_code_lens_in_file(
95+
&mut self,
96+
filename: &str,
97+
) -> Option<Vec<CodeLens>> {
98+
self.server
99+
.code_lens(CodeLensParams {
100+
text_document: TextDocumentIdentifier::new(url_from(filename)),
101+
work_done_progress_params: Default::default(),
102+
partial_result_params: Default::default(),
103+
})
104+
.await
105+
.unwrap_or_else(|_| panic!("unable to send code lens for filename {filename}"))
106+
}
94107
}
95108

96109
fn url_from(filename: &str) -> Url {

0 commit comments

Comments
 (0)