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
202 changes: 104 additions & 98 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sysdig-lsp"
version = "0.1.0"
version = "0.2.0"
edition = "2024"


Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm
| Feature | **[VSCode Extension](https://github.com/sysdiglabs/vscode-extension)** | **Sysdig LSP** |
|---------------------------------|------------------------------------------------------------------------|----------------------------------------------------------|
| Scan base image in Dockerfile | Supported | [Supported](./docs/features/scan_base_image.md) (0.1.0+) |
| Code lens support | Supported | In roadmap |
| Code lens support | Supported | [Supported](./docs/features/code_lens.md) (0.2.0+) |
| Build and Scan Dockerfile | Supported | In roadmap |
| Layered image analysis | Supported | In roadmap |
| Docker-compose image analysis | Supported | In roadmap |
Expand Down
Binary file added docs/features/code_lens.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/features/code_lens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Code Lens Support

A CodeLens is a feature that displays additional information about the code,
such as suggestions, references, or action buttons, directly in the editor without modifying the code itself.

For example, in VS Code, you might see something like "Used 3 times" above a function,
with an option to click and view references.

In our case, for the **Sysdig LSP**, **CodeLens** allows executing actions that can also be
performed with **Code Actions** (like [scanning the base image](./scan_base_image.md)), but in a more graphical way by simply clicking.
This enhances usability by providing direct interaction within the editor.

![Sysdig LSP code lens for base image scanning](./code_lens.gif)
2 changes: 1 addition & 1 deletion src/app/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ where
C: LSPClient,
{
pub async fn update_document_with_text(&self, uri: &str, text: &str) {
self.document_database.remove_document(uri).await;
self.document_database.write_document_text(uri, text).await;
self.document_database.remove_diagnostics(uri).await;
let _ = self.publish_all_diagnostics().await;
}

Expand Down
65 changes: 60 additions & 5 deletions src/app/lsp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use tower_lsp::LanguageServer;
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
use tower_lsp::lsp_types::{
CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse,
Command, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
ExecuteCommandOptions, ExecuteCommandParams, InitializeParams, InitializeResult,
InitializedParams, MessageType, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind,
CodeLens, CodeLensOptions, CodeLensParams, Command, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidOpenTextDocumentParams, ExecuteCommandOptions,
ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
Position, Range, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
};
use tracing::{debug, info};

Expand Down Expand Up @@ -106,6 +106,9 @@ where
TextDocumentSyncKind::FULL,
)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false),
}),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec![SupportedCommands::ExecuteScan.to_string()],
..Default::default()
Expand Down Expand Up @@ -180,7 +183,7 @@ where

if last_line_starting_with_from_statement == line_selected_as_usize {
let action = Command {
title: "Scan Image".to_string(),
title: "Scan base image".to_string(),
command: SupportedCommands::ExecuteScan.to_string(),
arguments: Some(vec![
json!(params.text_document.uri),
Expand All @@ -194,6 +197,58 @@ where
return Ok(None);
}

async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
info!("{}", format!("received code lens params: {params:?}"));

let Some(content) = self
.query_executor
.get_document_text(params.text_document.uri.as_str())
.await
else {
return Err(lsp_error(
ErrorCode::InternalError,
format!(
"unable to extract document content for document: {}",
&params.text_document.uri
),
));
};

let Some(last_line_starting_with_from_statement) = content
.lines()
.enumerate()
.filter(|(_, line)| line.trim_start().starts_with("FROM "))
.map(|(line_num, _)| line_num)
.last()
else {
return Ok(None);
};

let scan_base_image_lens = CodeLens {
range: Range {
start: Position {
line: last_line_starting_with_from_statement as u32,
character: 0,
},
end: Position {
line: last_line_starting_with_from_statement as u32,
character: 0,
},
},
command: Some(Command {
title: "Scan base image".to_string(),
command: SupportedCommands::ExecuteScan.to_string(),
arguments: Some(vec![
json!(params.text_document.uri),
json!(last_line_starting_with_from_statement),
]),
}),
data: None,
};

Ok(Some(vec![scan_base_image_lens]))
}

async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
let command: SupportedCommands = params.command.as_str().try_into().map_err(|e| {
lsp_error(
Expand Down
79 changes: 76 additions & 3 deletions tests/general.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use serde_json::json;
use tower_lsp::lsp_types::{CodeActionOrCommand, Command, MessageType};
use tower_lsp::lsp_types::{CodeActionOrCommand, CodeLens, Command, MessageType, Position, Range};

mod test;

Expand Down Expand Up @@ -34,7 +34,7 @@ async fn when_the_client_asks_for_the_existing_code_actions_it_receives_the_avai
assert_eq!(
response.unwrap(),
vec![CodeActionOrCommand::Command(Command {
title: "Scan Image".to_string(),
title: "Scan base image".to_string(),
command: "sysdig-lsp.execute-scan".to_string(),
arguments: Some(vec![json!("file://dockerfile/"), json!(0)])
})]
Expand Down Expand Up @@ -62,9 +62,82 @@ async fn when_the_client_asks_for_the_existing_code_actions_but_the_dockerfile_c
assert_eq!(
response_for_second_line.unwrap(),
vec![CodeActionOrCommand::Command(Command {
title: "Scan Image".to_string(),
title: "Scan base image".to_string(),
command: "sysdig-lsp.execute-scan".to_string(),
arguments: Some(vec![json!("file://dockerfile/"), json!(1)])
})]
);
}

#[tokio::test]
async fn when_the_client_asks_for_the_existing_code_lens_it_receives_the_available_code_lens() {
let mut client = test::TestClient::new_initialized().await;

// Open a Dockerfile containing a single "FROM" statement.
client
.open_file_with_contents("Dockerfile", "FROM alpine")
.await;

// Request code lens on the line with the FROM statement (line 0).
let response = client
.request_available_code_lens_in_file("Dockerfile")
.await;

// Expect a CodeLens with the appropriate command.
assert_eq!(
response.unwrap(),
vec![CodeLens {
range: Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 0
}
},
command: Some(Command {
title: "Scan base image".to_string(),
command: "sysdig-lsp.execute-scan".to_string(),
arguments: Some(vec![json!("file://dockerfile/"), json!(0)])
}),
data: None
}]
);
}

#[tokio::test]
async fn when_the_client_asks_for_the_existing_code_lens_but_the_dockerfile_contains_multiple_froms_it_only_returns_the_latest()
{
let mut client = test::TestClient::new_initialized().await;
client
.open_file_with_contents("Dockerfile", "FROM alpine\nFROM ubuntu")
.await;

let response = client
.request_available_code_lens_in_file("Dockerfile")
.await;

assert_eq!(
response.unwrap(),
vec![CodeLens {
range: Range {
start: Position {
line: 1,
character: 0
},
end: Position {
line: 1,
character: 0
}
},
command: Some(Command {
title: "Scan base image".to_string(),
command: "sysdig-lsp.execute-scan".to_string(),
arguments: Some(vec![json!("file://dockerfile/"), json!(1)])
}),
data: None
}]
);
}
23 changes: 18 additions & 5 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use sysdig_lsp::app::{LSPClient, LSPServer};
use tokio::sync::Mutex;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::{
CodeActionOrCommand, CodeActionParams, Diagnostic, DidOpenTextDocumentParams, InitializeParams,
InitializeResult, InitializedParams, MessageType, Position, Range, TextDocumentIdentifier,
TextDocumentItem, Url,
CodeActionOrCommand, CodeActionParams, CodeLens, CodeLensParams, Diagnostic,
DidOpenTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
Position, Range, TextDocumentIdentifier, TextDocumentItem, Url,
};

pub struct TestClient {
Expand Down Expand Up @@ -86,11 +86,24 @@ impl TestClient {
.await
.unwrap_or_else(|_| {
panic!(
"unable to send code action for filename {} in line number {}",
filename, line_number
"unable to send code action for filename {filename} in line number {line_number}",
)
})
}

pub async fn request_available_code_lens_in_file(
&mut self,
filename: &str,
) -> Option<Vec<CodeLens>> {
self.server
.code_lens(CodeLensParams {
text_document: TextDocumentIdentifier::new(url_from(filename)),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await
.unwrap_or_else(|_| panic!("unable to send code lens for filename {filename}"))
}
}

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